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.
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. 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.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!
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.
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
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.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!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.