No one needs to read again about why tests are important and we use tests in all languages and frameworks. And you've probably faced the need to mock some calls or objects during testing.
But firstly, let's refresh our knowledge about these ideas:
mock objects are simulated objects that mimic the behavior of real objects in controlled ways, most often as part of a software testing initiative (from Wikipedia).
In Ruby, we have some utilities that provide abilities to mock anything that you need to mock easily. I will use Rspec
for my example.
Just imagine that we have the next code:
class Survey
class Client
def publish
true
end
end
def initialize(client:)
@client = client
end
def publish
@client.publish
end
end
And we need to test this class, the straightforward approach can be next:
require "./survey"
RSpec.describe Survey do
it "publish survey" do
client = double("client")
expect(client).to receive(:publish).and_return(true)
Survey.new(client: client).publish
end
end
Looks good, the test passes, but what if we change the method for Survey::Client from publish
to boom
:
class Client
def boom
true
end
end
what will happen?
rspec ./survey_spec.rb
.
Finished in 0.01933 seconds (files took 0.30562 seconds to load)
1 example, 0 failures
it passed, how can it be? The answer is - double
.
How can we fix the problem???
Solution 1 (old school)
What I don't like in languages like Ruby is all objects are quite wage and you need some mental energy to keep track of what really happens in your code. What does Survey
expect as an input parameter? Survey::Client? It's easy to think like this but in reality, it accepts any object with a specific interface. So what we send as a parameter to Survey should play a role. Thus in order to avoid this problem we need to check if our client can play this role:
# add a shared example
RSpec.shared_examples "PublisherRole" do
it "should response to publish" do
expect(object.respond_to?(:publish)).to be true
end
end
# add test for our client
RSpec.describe Survey::Client do
let(:object) { Survey::Client.new }
include_examples "PublisherRole"
end
and now we get:
$ rspec survey_spec.rb
.F.
Failures:
1) Survey::Client should response to publish
Failure/Error: DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
expected true
got false
Shared Example Group: "PublisherRole" called from ./survey_spec.rb:17
# ./publisher_role.rb:3:in `block (2 levels) in <top (required)>'
Finished in 0.05281 seconds (files took 0.34183 seconds to load)
3 examples, 1 failure
Failed examples:
rspec ./survey_spec.rb:15 # Survey::Client should response to publish
Solution 2 (use RSpec specifics)
What allows you to achieve the same result with less code? - instance_double
:
RSpec.describe Survey do
it "publish survey" do
# use instance_double instead of double
# client = double("client")
client = instance_double("Survey::Client", publish: true)
expect(client).to receive(:publish).and_return(true)
Survey.new(client: client).publish
end
end
Conclusion
The main idea of my post is - to be aware of what you have and how you test it. Try to think maybe in one level up, not just test direct calls and objects.
Top comments (0)