You stub/mock incorrectly
Tags: ruby, testing, mock
Published: 2024-05-13 15:47:26



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:

```ruby
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:
```ruby
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`:
```ruby
  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:
```ruby
# 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:

```bash
$ 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`:

```ruby
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.