Let's say I have:
class Foo
def do_thing(arg_one:, arg_two:)
# ...
end
end
class Bar
def initialize(foo)
@foo = foo
end
def do_delegated_thing
@foo.do_thing(arg_one: "x", arg_two: "y")
end
end
and a test:
describe Bar do
let(:mock_foo) do
foo = double(Foo)
allow(foo).to receive(:do_thing)
foo
end
let(:subject) { described_class.new(mock_foo) }
it "calls Foo with expected arguments" do
subject.do_delegated_thing
expect(mock_foo).to have_received(:do_thing).with(arg_one: "x", arg_two: "y")
end
end
Now let's say I want to refactor Foo
and change an argument name:
class Foo
def do_thing(arg_one_hundred:, arg_two:)
# ...
end
end
The test will still pass, even though the expectation receive(:do_thing).with(arg_one: "x", arg_two: "y")
is now invalid. In my mind, do_thing
should not be able to be expected to be called with invalid arguments. Is there a sane way around this, i.e. a better RSpec API method I should be using to ensure that the expectation is actually legitimate?
What you are looking for is a Verifying Double in this case an instance_double
.
From the Docs (emphasis added):
An
instance_double
is the most common type of verifying double. It takes a class name or object as its first argument, then verifies that any methods being stubbed would be present on an instance of that class. In addition,when it receives messages, it verifies that the provided arguments are supported by the method signature, both in terms of arity and allowed or required keyword arguments, if any.
For Example, if you change foo = double(Foo)
to foo = instance_double(Foo)
your original test will pass and when you change the method to def do_thing(arg_one_hundred:, arg_two:)
it will fail.
class Foo
def do_thing(arg_one:, arg_two:); end
end
class Baz
def do_thing(arg_one_hundred:, arg_two:); end
end
class Bar
def initialize
@foo = Foo.new
end
def do_delegated_thing
foo.do_thing(arg_one: "x", arg_two: "y")
end
private
attr_reader :foo
end
describe Bar do
let(:mock_foo) {instance_double(Foo, do_thing: nil)}
let(:mock_baz) {double(Baz, do_thing: nil)}
let(:mock_baz_2) {instance_double(Baz, do_thing: nil)}
let(:subject) { described_class.new }
it "calls Foo with expected arguments" do
allow(subject).to receive(:foo).and_return(mock_foo)
expect(mock_foo).to receive(:do_thing).with(arg_one: "x", arg_two: "y")
subject.do_delegated_thing
end
context 'comparing double and instance_double' do
it "calls double with unexpected arguments" do
allow(subject).to receive(:foo).and_return(mock_baz)
expect(mock_baz).to receive(:do_thing).with(arg_one: "x", arg_two: "y")
subject.do_delegated_thing
end
it "calls instance_double with unexpected arguments" do
allow(subject).to receive(:foo).and_return(mock_baz_2)
expect(mock_baz_2).to receive(:do_thing).with(arg_one: "x", arg_two: "y")
subject.do_delegated_thing
end
end
end
Output:
Bar
calls Foo with expected arguments
comparing double and instance_double
calls double with unexpected arguments
calls instance_double with unexpected arguments (FAILED - 1)
Failures:
1) Bar comparing double and instance_double calls instance_double with unexpected arguments
Failure/Error: expect(mock_baz_2).to receive(:do_thing).with(arg_one: "x", arg_two: "y")
Missing required keyword arguments: arg_one_hundred
# ./spec.rb:42:in `block (3 levels) in <top (required)>'
# main.rb:22:in `<main>'
Finished in 0.00752 seconds (files took 0.0997 seconds to load)
3 examples, 1 failure