DEV Community

Cover image for You stub/mock incorrectly
Aleksandr Korolev
Aleksandr Korolev

Posted on

You stub/mock incorrectly

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

what will happen?

 rspec ./survey_spec.rb . Finished in 0.01933 seconds (files took 0.30562 seconds to load) 1 example, 0 failures 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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)