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)