As you start to get more familiar with automated testing, at some point you will likely hear other developers say things like:
“You know you can just stub that method, right?”
“This is a great candidate for a double!”
And at the beginning these things can be confusing to understand. π
So let’s start by defining each term, followed by their practical use, in specific relation to rspec
within a rails
project.
Definitions π
One of the tricky things I have seen when these concepts are defined is that they are often defined within the context of testing.
For me, that just adds a layer of complexity.
It is true that these concepts are often used within the context of testing, but they don’t have to be.
So let’s take a stab or should I say stub π at defining them without talking about tests.
Stubbing
Stubbing is just a term for defining a method on an object.
However, it indicates the short-term nature of that method.
If you stub a method on an object, your intention is that it exists for a short period of time and most likely returns a simple hard-coded value.
You may use a stub to define a method that does not exist yet on an object or to override an existing method to get it to return a value you desire.
For example:
class Feature
def self.available?(name)
# all features are disabled by default
false
end
end
puts Feature.available?("user_preference")
# => false
# stub: override the method to return a canned response
def Feature.available?(name)
true
end
puts Feature.available?("user_preference")
# => true
Doubles
A double, is simply an object that is a double of another object.
You may be thinking, “but why would you do that?!”, but lets put that question to one side for now just so we can understand the concept in its purest form.
A double of something is just a copy of that thing.
If I have one chocolate and I double that chocolate I now have two of the same chocolate. We can call the first chocolate the original and the second chocolate a double of the original. yum. π«
Great Success!! We made it this far and we haven’t talked about tests!
Testing π§ͺ
Hopefully now that we understand these concepts independent of testing we can talk a little bit about how they can be helpful tools to use when testing.
Stubbing
Sometimes when you are testing a method, the method under test will send a message to another object in order to retrieve some value.
That sounds confusing but look at this example:
class EasyMath
def sum(a,b)
if SumFeature.available?
a + b
end
end
end
The available?
message is being sent to another class SumFeature
in order to return the value true
or false
.
When we come to test the behaviour of the sum
method, it would be really useful if we could just worry about
a + b
without having to be concerned with what the available?
method does.
At the end of the day, the available?
method belongs to the SumFeature
class and has nothing to do with us over here and we can assume it is tested in the SumFeature
class where it is defined.
Well thankfully we have a tool in our arsenal called stubbing
that will allow us to stub
the available?
method so it always returns true
.
This is great! We can now focus on testing a + b
whilst relying on available?
to always return true
.
Ok, but what does this look like in a test?
describe "#sum" do
it "adds two numbers together and returns the total" do
easy_math = EasyMath.new
allow(SumFeature).to receive(:available?).and_return(true)
total = easy_math.sum(2,2)
expect(total).to eq(4)
end
end
See that line allow(SumFeature).to receive(:available?).and_return(true)
?
The available?
method has now been stubbed out to always return true
.
No matter how many times you call SumFeature.available?
in your application code, it will always return true
.
We did a thing! We stubbed a method! And as you can see it had nothing to do with doubles! π₯
Doubles
Ok if you have made it this far, then stay with me as we are nearly there..
Let’s take the same example we used above with the sum
method, but tweak it slightly, to see how we could use a double
.
class EasyMath
def sum(a,b,user)
if user.likes_math?
a + b
end
end
end
Once again we are sending a message likes_math?
somewhere else in order to retrieve a value of true
or false
.
Similarly we only want to concern ourselves with the behaviour of a + b
without having to worry about likes_math?
.
Given what we know so far, we might suggest stubbing the likes_math?
method in the same way we stubbed available?
.
allow(user).to receive(:likes_math?).and_return(true)
However, whilst a class method like available?
can be stubbed without the need for a double
, an instance method does require
a double
before it can be stubbed. (There are exceptions to this by using any_instance_of
but it is not recommended.
In other words we first need to create a double
of the user
object before we can stub the likes_math?
method on it, to
always return true
for us.
Now hold on one second!! Why am I talking about stubbing
alongside doubles
? Didn’t I kind of say I didn’t want to do that?!
Ok hear me out on this one…
Whilst stubbing
and doubles
are completely separate concepts, a double
is only meaningful and useful once you stub
methods on it.
This is because when you create a double
you are creating a double
of that object without any methods on it.
The double
is just a plain Ruby Object
.
Therefore, in reality, although stubbing
is used on its own, when you are using a double
you will likely need to use stubbing
as well.
Ok so lets see how the code would look for testing our sum
method:
describe "#sum" do
it "adds two numbers together and returns the total" do
easy_math = EasyMath.new
user_double = double
allow(user_double).to receive(:likes_math?).and_return(true)
total = easy_math.sum(2,2,user_double)
expect(total).to eq(4)
end
end
See how we first create a double
like this user_double = double
.
And on the next line we stub
the likes_math?
method on to our double
like this: allow(user_double).to receive(:likes_math?).and_return(true)
.
It might be helpful to think about it this way:
A double
is a useful tool to help you to be able to easily stub
.
And that’s really all there is to it folks!
But before I go, let’s wrap up with some helpful tips.
Take-Away Tips πͺΆ
Stubbing
- Stubbing has nothing to do with doubles.
- It helps us to force the return value of a method we don’t care about in our test because it is tested elsewhere.
- A class method can be stubbed without the need for a double.
- An instance method will need a double (unless you use
any_instance_of
).
Doubles
- Doubles are a useful way to create an object that acts as a double of another object.
- The most basic way to create a double in
rspec
is to use thedouble
method. - Another option is to use
instance_double(User)
. This is the same asdouble
but will verify that any methods you stub exist on the class you are doubling. - If you are using
FactoryBot
you can usebuild_stubbed(:user)
which will create aninstance_double
but with the methods stubbed out as set in the factory. It also has the added advantage of stubbing out associated objects and created timestamps.