Using contracts.ruby With RSpec

contracts.ruby, design-by-contract, rspec, ruby

Issues with RSpec mocks

Lets start from example:

1
2
3
4
5
6
7
8
class Example
  include Contracts

  Contract Something => Any
  def do_something(something)
    something.call
  end
end

And its corresponding spec:

1
2
3
4
5
6
7
8
9
10
11
require "rspec"

RSpec.describe Example do
  let(:example) { Example.new }
  let(:something) { instance_double(Something, call: :hello) }

  it "works" do
    expect(example.do_something(something))
      .to eq(:hello)
  end
end

Pretty straightforward unit test for Example#do_something. But if you run rspec you will get:

1
2
3
4
5
ContractError: Contract violation for argument 1 of 1:
    Expected: Something,
    Actual: #<RSpec::Mocks::InstanceVerifyingDouble:0xa401a0 @name="Something (instance)">
    Value guarded in: Example::do_something
    With Contract: Something => Any

It happens because class contracts use #is_a? to determine if contract matches or not. Simply: something.is_a?(Something) is required to be true.

But if we try to do it with instance_double, that is what we get:

1
2
3
4
require "rspec/mocks/standalone"
something = instance_double(Something)
something.is_a?(Something)                              #=> false
something.is_a?(RSpec::Mocks::InstanceVerifyingDouble)  #=> true

Solution to problem

Pretty straightforward one:

1
2
3
4
5
6
7
8
let(:something) { instance_double(Something, call: :hello) }

before do
  allow(something)
    .to receive(:is_a?)
    .with(Something)
    .and_return(true)
end

But this can be boring to type it each time you need an instance_double while working with contracts. So here you go:

1
2
3
4
# Gemfile
group :test do
  gem "contracts-rspec"
end

Run bundle to install contracts-rspec gem.

1
2
3
4
5
6
7
8
# your spec file
require "contracts/rspec"

RSpec.describe Example do
  include Contracts::RSpec::Mocks

  # .. write code as in first example ..
end

Now you are covered. Inclusion of Contracts::RSpec::Mocks slightly alters behavior of instance_double. Now it automatically stubs out #is_a?(Klass) to return true on the class it was created from. In our case Something. This happens here: https://github.com/waterlink/contracts-rspec/blob/master/lib/contracts/rspec/mocks.rb#L4-L8

You can include it only in contexts you need, or you can do it globally from spec_helper like you do usually include spec helpers.

Links

Thanks!

If you have any questions, suggestions or just want to chat about how contracts.ruby is awesome, you can ping me on twitter @tdd_fellow. If you have any issues using contracts.ruby or contracts-rspec you can create issues on corresponding github project. Pull requests are welcome!