Introduction
Testing is probably one of the most popular topics in the Rails community today. This is because we are yet to get to the point where testing has the same wow factor like the other parts of Rails.
The problem with testing and writing Rails code is that we can’t quite get the separation of concerns that OOP (Object Oriented Programming) advocates, so we’re stuck somewhere in the middle, trying to get the most out of the Rails magic, while also squeezing out some separation.
In this article, we’ll explain what verifying doubles are, why they were introduced in RSpec 3, and how they can help you improve your tests.
Regular Doubles
You’ve probably heard of test doubles before. If not, you can check out this introduction. Test doubles are what we use in our tests when we need an object that is a placeholder for, and that should mimic the behavior of the real object that the object under test depends on. The reason why we don’t use the real object is isolation — we want to isolate the object under test from the rest of the world, the rest of the world being the objects it depends on in this case.
To start with, let’s take a look at a theoretical example of isolation.
Say we have ObjA
that represents the object we want to test. This object might have a dependency on ObjB
which is not the focus of our test. ObjB
is what you will want to replace with a test double in order to have full control of its behavior.
The ObjB
’s double is not the real object that ObjA
depends on, it just mimics its behavior.
Stubbing Done Right
You might be wondering what is the problem with stubbing. Well, in the theoretical example above we showed that the double is not the real object, but it mimics the real object. This is a good thing, because we can control its behavior — we can make it do whatever we want. However, this can also be a problem, since the double can differ from the real object. This difference usually becomes problematic later, when the real object and its double get out of sync, e.g. due to changing method names of the real object.
For example, our double might respond to a message like .name
, but the real object can change the method name at some point to .full_name
. In this case, our tests will still pass since they are disconnected from the real object and are based on the test double instead.
This is what verifying doubles are supposed to fix. They provide a mechanism that keeps the double and the real object in sync. That way, we know when the real object tries to diverge from its double, which provides us with a more reliable test suite. The only thing worse than not having an automated test suite is having one that gives you false feedback.
Let’s See Some Code
To illustrate the point, let’s write a test that uses a regular double and one that uses a verifying double.
# rspec spec/models/account_spec.rb require 'rails_helper'
class Account
end
RSpec.describe Account do
describe "regular double" do
it "calls #name" do
account = double(name: "Joe")
expect(account.name).to eq("Joe")
end
end
describe "verifying instance double" do
# This one will fail since the name method doesn't exist in the
# Account instance
it "calls #name" do
account = instance_double("Account", name: "Joe")
expect(account.name).to eq("Joe")
end
end
describe "regular class double" do
it "calls .birthday" do
account = double("Account", birthday: "01/01/1970")
expect(account.birthday).to eq("01/01/1970")
end
end
describe "verifying class double" do
# This one will fail since the birthday method doesn't exist on the
# Account class
it "calls .birthday" do
account = class_double("Account", birthday: "01/01/1970")
expect(account.birthday).to eq("01/01/1970")
end
end
end
Running the test above, we can see that two examples pass, and two fail.
$ rspec spec/models/account_spec.rb -f doc
.F.F
Failures:
1) Account verifying instance double calls #name
Failure/Error: account = instance_double("Account", name: "Joe")
the Account class does not implement the instance method: name. Perhaps you meant to use class_double instead?
# ./spec/models/account_spec.rb:15:in `block (3 levels) in <top (required)>'
2) Account verifying class double calls .birthday
Failure/Error: account = class_double("Account", birthday: "01/01/1970")
the Account class does not implement the class method: birthday
# ./spec/models/account_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.00182 seconds (files took 0.08286 seconds to load)
4 examples, 2 failures
Failed examples:
rspec ./spec/models/account_spec.rb:14 # Account verifying instance double calls #name
rspec ./spec/models/account_spec.rb:28 # Account verifying class double calls .birthday
In the above example, we have created an instance double with RSpec’s instance_double
method, which checks if the method we are calling on the instance double is also available on the Account
class as an instance method.
Similarly, we have created a class double with the use of RSpec’s class_double
method, which checks if the method we are calling on the class double is also available on the Account
class as a class method.
In both cases, if the method we are calling on the double is not defined in its class, the test will fail with a message that informs us that something is out of sync.
Verifying Doubles are Not Perfect
There is one gotcha with verifying doubles, especially for Rails developers, which involves the methods that are dynamically defined, e.g. with method_missing
. Since in Ruby methods can be created dynamically, verifying doubles have no way of knowing that those methods exist on the real object.
For example, ActiveRecord
creates methods for each of the model’s attributes dynamically. So, even if those methods exist on the ActiveRecord
object, because they are the model’s attributes, they are dynamically created by ActiveRecord
, and the verifying double won’t know about them.
However, there is a workaround that is already part of the rspec-rails
gem, so you don’t have to worry about it anymore if you’re using rspec-rails
. So, just put the rspec-rails
gem in your Rails project, and you’re good to go.
If for some reason you’re not using the rspec-rails
gem — e.g. you’re using Ruby without Rails and you stumble upon some dynamic methods — you’ll want to take a look at the RSpec documentation for verifying doubles, which describes how you can use the workaround in your tests.
Conclusion
Verifying doubles are so useful that we encourage you to always use them instead of their non-verifying versions. They will prove useful when the real objects change by letting you know about the change.
Overall, verifying doubles make the whole test less brittle by keeping the stubbed object and its double in sync, thus preventing API changes from going unnoticed in tests.
P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.