paint-brush
How to Use RSpec From Basics to Testing User Inputby@luis-novoa
3,993 reads
3,993 reads

How to Use RSpec From Basics to Testing User Input

by Luis NovoaFebruary 1st, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Ruby gem RSpec is a Ruby gem for Test-Driven Development. You create tests as you code, so in the future, if an update breaks something, it will be easy to notice because one or more tests will fail. If you don't have RSpec installed on your machine, follow this guide to know how to get it. RSpec's syntax lets us create various scopes which we can use to share variables through tests. The method defines each test, and it is possible to share objects and tests across scopes.

Company Mentioned

Mention Thumbnail
featured image - How to Use RSpec From Basics to Testing User Input
Luis Novoa HackerNoon profile picture

On my journey on Microverse, one of the things that troubled me the most to understand was RSpec, a Ruby gem for Test-Driven Development. The concept is quite simple. You create tests as you code along, so in the future, if an update breaks something, it will be easy to notice because one or more tests will fail.

If you don't have RSpec installed on your machine, follow this guide to know how to get it. Aside from the installation, it explains how to apply RSpec to a file, and it also gives some details about the tests' output.

The first steps were easy to follow, but one challenge arose: testing user input. Since I don't want to lose the knowledge I just acquired, and I had trouble finding guides on this matter, I want to make my life and the others easier writing this article, so I can use it later as reference, or at least, a quick reminder.

Scopes

RSpec's syntax lets us create various scopes which we can use to share variables through tests.

RSpec.describe Class do
  # Declare variables that are readable by all tests of this class

  describe '#method' do
    # Declare variables that are readable by all tests of this method

    it 'nice description of what this test is testing' do
      # Test especifications
    end

    it 'you can have more than one test per method!' do
      # Different test especifications
    end
  end

  describe '#another_method' do
    # You can describe as many methods as you want
    # inside a class!

    it 'another_methods test' do
	#Especifications
    end
  end
end

RSpec.describe DifferentClass do
  # Create different scopes for each class!
end

Notice that on

RSpec.describe Class do
,
Class
isn't a string. The
it
method defines each test you want to implement. Also, it is possible to share objects and tests across scopes. Check the documentation for more about this feature.

Variables

RSpec provides the

let
method for variable declaration. The traditional way would also work, but
let
creates the variables only when required by the tests, which saves memory and accelerates the computing process. You can also use
let!
if you want the variable loaded before the test execution.

# These two methods of variable declaration are analogous

let(:var) { Class.new }
var = Class.new

The variable name must always be a symbol in the

let
method, and its content can be any primitive data type or object.

# It also works for doubles

let(:var) { double('class') }
var = double('class')

The

double()
method creates a generic object, to which you can attribute behaviors (even though you can do that for real objects too). The
'class'
string is descriptive and doesn't link the object to any class in specific.

The Example Code

From this point on, we'll start to write tests for two classes:

ClassyClass
and
Citizen
. For convenience, I will display both the method tested and the test in the same block of code, but in your work, you should have a file dedicated to the core program, and another file for all your tests.

I also created a repository where I stored my test examples. Check it out and feel free to clone it!

Basic Test Structure

In this example, we want to test the

sum()
method of the
Citizen
class.

#========== Class ==========

class Citizen
  attr_accessor :name, :job
  def initialize(name, job)
    @name = name
    @job = job
  end

  def sum(num1, num2)
    output = num1 + num2
    output
  end
end

#========== Test ==========

RSpec.describe Citizen do
  describe '#sum' do
    let(:accountant) { Citizen.new('James', 'Accountant') }

    it 'returns the sum of two numbers' do
      expect(accountant.sum(1, 2)).to eq(3)
    end
  end
end

The first step on your test is to create an instance of the object you want to test. After that, you create a Mock using the

expect()
method. A mock is the requirement for your test to succeed or fail.

In this case, we mocked the behavior of the

account.sum()
method when it receives
1
and
2
as inputs and expected it to return a value equals to
3
, represented by the matcher
eq(3)
.

RSpec provides a wide variety of matchers, and even the possibility to create custom ones. Check the full list of matchers to find the best for what you want to test.

Testing Console Output

RSpec lets you test the messages your program will output to the console. The Mocking process is similar, but with a few more elements attached to the

expect()
method. Check below.

#========== Class ==========

class ClassyClass
  def exists?
    puts 'Yes'
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let(:stacey_instance) { described_class.new }
  describe '#exists?' do
    it 'outputs a confirmation that it exists' do
      expect { stacey_instance.exists? }.to output("Yes\n").to_stdout
    end
  end
end

On

ClassyClass
, we defined the function
exists?
which outputs
Yes
on the console when called. A simple case for a simple test.

The procedure is the same as before, but here we need to use the

output()
matcher to write what the expected output would be, and we need
.to_stdout
to specify that the content will show up in the console.

On our test object, we used the built-in

described_class.new
method to create a new instance of
ClassyClass
. This is a feature offered by RSpec, but if you prefer, you can always use
ClassyClass.new
to achieve the same effect.

Since we are testing the

puts
method, we need to include
\n
in our expectation, which represents the line-break on Ruby. We could take out
\n 
if we were testing the
print
method.

We can also test multi-line outputs using the same configuration. We just need to add

\
between each line and we're set!

#========== Class ==========

class Citizen
  attr_accessor :name, :job
  def initialize(name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen(citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job}"
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let(:stacey_instance) { described_class.new }
  let(:real_deal) { Citizen.new('John', 'Software Developer') }
  let(:double_trouble) { double('citizen') }

  describe '#check_citizen' do
    it "tells double_trouble's specific name and job" do
      allow(double_trouble).to receive(:name).and_return('John')
      allow(double_trouble).to receive(:job).and_return('Software Developer')

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end

    it "tells real_deal's specific name and job" do
      expect { stacey_instance.check_citizen(real_deal) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end

    it "tells double_trouble's specific name and job using expect" do
      expect(double_trouble).to receive(:name).and_return('John')
      expect(double_trouble).to receive(:job).and_return('Software Developer')

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end
  end
end


Doubles, Stubs and Mocks

Disclaimer: the difference between these terms is very subtle, and in my researches, I noticed that there is overlap between them. I will write down my impressions and what made sense for me when it comes to understand them, but remember that I'm a student, and I don't have the authority or capacity to strictly define those terms yet. For better understanding, please check the references I left at the end of this article.

As mentioned before, doubles are generic objects. They do nothing on their own, and you need to assign responses to them through stubs or mocks. They are useful when you're still not certain about the object's class.

The most common definition I found for stubs is that they are objects with canned responses. I prefer to think of the stub as the canned response, and the objects that are modified to be called stubbed objects. You can stub an object using the

allow()
method.

#========== Class ==========

class Citizen
  attr_accessor :name, :job
  def initialize(name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen(citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job}"
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let(:stacey_instance) { described_class.new }
  let(:real_deal) { Citizen.new('John', 'Software Developer') }
  let(:double_trouble) { double('citizen') }

  describe '#check_citizen' do
    it "tells double_trouble's specific name and job" do
      allow(double_trouble).to receive(:name).and_return('John')
      allow(double_trouble).to receive(:job).and_return('Software Developer')

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end

    it "tells real_deal's specific name and job" do
      expect { stacey_instance.check_citizen(real_deal) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end
  end
end

In the first test, you can see the

allow()
method in action. First, you specify which variable you want to stub (
double_trouble
in this case). Then you use the
receive()
method to tell which method should be available for that object, and
and_return()
to determine how it will respond when the method is called.

In short, we can say that

allow(double_trouble).to receive(:name).and_return
('John')

is a way to guarantee that double_trouble will behave as

double_trouble.name = 'John'
.

Remember that

double_trouble
isn't an instance of
Citizen
. Stubbing or mocking lets us give any behavior we want to any object, which is useful for cases where we don't have a class specified, but we know how a method should treat an object. We gear a double object so it acts as a real one.

The second test confirms that. The object

real_deal
is an instance of
Citizen
with the same attributes of the stubbed object
double_trouble
. The output is the same for both scenarios and the tests pass.

We can also have a more generalist approach, not giving any specific value for

.name
and
.job
.

#========== Class ==========

class Citizen
  attr_accessor :name, :job
  def initialize(name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen(citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job}"
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let(:stacey_instance) { described_class.new }
  let(:double_trouble) { double('citizen') }

  describe '#check_citizen' do
    it "tells double_trouble's generic name and job" do
      allow(double_trouble).to receive(:name)
      allow(double_trouble).to receive(:job)

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is #{double_trouble.name} and he works as #{double_trouble.job}\n"
      ).to_stdout
    end
  end
end

We just stubbed the ability to respond to calls for

.name
and
.job
, and in the output, we expect it to call for
#{double_trouble.name}
and
#{double_trouble.job}
.

This implies that it doesn't matter what are the values of those two attributes. The

check_citizen()
method will figure out how to handle them and will produce the output accordingly.

That's enough of stubbing, now let's talk about Mocking.

#========== Class ==========

class Citizen
  attr_accessor :name, :job
  def initialize(name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen(citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job}"
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let(:stacey_instance) { described_class.new }
  let(:double_trouble) { double('citizen') }

  describe '#check_citizen' do
        it "tells double_trouble's specific name and job using expect" do
      expect(double_trouble).to receive(:name).and_return('John')
      expect(double_trouble).to receive(:job).and_return('Software Developer')

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end
  end
end

The test using mocking is pretty much the same as using stubbing, with the difference of using

expect()
instead of
allow()
. That's possible because, for each scenario, only the last
expect()
is treated as an uncertain expectation. The others are treated as expectations taken for granted.

Conceptually, they also aren't the same. When you stub an object, you are giving actions to them, while in the case of mocking, you assume that they already can perform those actions.

This is a very subtle difference, and in my experience as a beginner, I still didn't find a case where there's a practical difference in the results of these techniques.

Maybe on more complex scenarios, it's useful to apply one or another. For now, I prefer to use

allow()
because it's easier to catch at a glance which lines are giving actions and which one is the expectation.

User Input

To emulate user input isn't hard, but can trick a beginner into spend hours of research, especially if he/she is stubborn like me and wants the most vanilla solution possible.

Well, the solution I came up is pretty vanilla (which means you don't need to install additional gems), but it does need us to require a class that isn't loaded by default on the ruby sessions. So bare with me and don't mind this "transgression" of the vanilla ways, it pays off when you realize that you'll have another tool at your disposal.

The class I'm talking about is called

StringIO
. It creates objects that store one or more strings. When associated with the variable
$stdin
, this object will provide its stored content whenever the code calls for user input.

Let's play a little with this object on IRB, so you get used to it! Call

irb
on your terminal and follow the instructions bellow.

require 'stringio'

io = StringIO.new('Hi!')
# The string is optional. You can create an empty StringIO 
# if you want.

io.puts 'Hello!'
# This is another way of adding strings to the object

$stdin = io
# Plug the object into $stdin so it uses io inputs instead of asking 
# the user for it

gets
gets
gets
# Call gets two times, and you'll receive 'Hi!' and 'Hello!', from 
# the third time on, you'll get nil.

io.rewind
# Sets the pointer of the object to the first string

gets
# Calling gets returns 'Hi!' again!

$stdin = STDIN
# After experimenting, set $stdin back to its original.

If you feel curious about

STDIN
, you can check this article, but basically, it's a constant that "listens" to the user input and pass it to the ruby program through the
$stdin
variable.

Now that you have a good grasp on

StringIO
objects, let's proceed to the test.

#========== Class ==========

class Citizen
  attr_accessor :name, :job
  def initialize(name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def change_name(citizen_instance)
    puts "What's the new name of the citizen?"
    citizen_instance.name = gets.chomp
  end
end

#========== Test ===========

require 'stringio'

RSpec.describe ClassyClass do
  let(:stacey_instance) { described_class.new }

  describe '#change_name' do
    let(:input) { StringIO.new('Larry') }
    let(:new_deal) { Citizen.new('Mark', 'Banker') }

    it "receive user input and change citizen's name" do
      $stdin = input
      expect { stacey_instance.change_name(new_deal) }.to output(
        "What's the new name of the citizen?\n"
      ).to_stdout.and change { new_deal.name }.to('Larry')

      $stdin = STDIN
    end
  end
end

My goal is to test the ability of the method

change_name()
to change the
name
attribute of a
Citizen
object using a string provided by the user.

The first step is to create the

StringIO
object with the string you want to pass as user input and plug it on
$stdin
. Next, you define your expectations with the
expect()
method.

At first, I created this test with only one expectation, but the question always showed up in the RSpec output. The solution for this was to create another expectation that tests the output generated by the

change_name()
method.

The second expectation was added through the

.and
method. Then, I used
change()
to test if
change_name()
is capable of changing the
name
attribute of the
new_deal
object to the string provided by
input
.

After that, I restored the

$stdin
to its default configuration. The test works! This is how you test user input!

Conclusion

RSpec provides way more possibilities than what I showed in this article, but I'm confident that following the examples I gave and having all these concepts in mind, a beginner can start writing tests right from the bet.

References