Dry::Matcher.for incompatible with RSpec mocks

I’m defining a boundary between the delivery mechanism (web) and the business logic. I want to test the delivery mechanism in isolation, and to achieve that I’m stubbing the business logic.

Given the following code on an endpoint:

post '/people' do
  Operations::People::CreatePerson.new.call(declared(params)) do |m|
    m.success do |person|
      status 201
      person
    end

    m.failure do |errors|
      status 422
      errors
    end
  end
end

The following operation:

module Operations
  module People
    class CreatePerson
      include Dry::Matcher.for(:call, with: ComplexMatcher)
      include Operations::Result::Mixin

      def call(person_attributes)
        person = Person.create!(person_attributes)

        Success(value: person)
      rescue ActiveRecord::ActiveRecordError => error
        Failure(value: error.message, code: :not_created)
      end
    end
  end
end

And the following stub in an endpoint test:

allow_any_instance_of(Operations::People::CreatePerson).to receive(:call).and_return(result)

I get this error:

Using any_instance to stub a method (call) that has been defined on a prepended module (#<Module:0x007ff2d3bda8d0>) is not supported.

This happens because Dry::Matcher.for prepends a module when included.

Is there a way to overcome this problem?

Thanks

I know this isn’t a specific answer for your situation, but the general answer is dependency injection. The controller is given the operation as an object, and that object can be easily mocked out. That’s the idea behind dry-auto_inject.

For your particular situation, you could stub out Operations::People::CreatePerson.new to return a mock, and then have a second stub for call on that mock. Another option would be to wrap the call to the operation in another method – something like:

call_operation(Operations::People::CreatePerson, declared(params))

Then you can stub that one method.

1 Like

Yeah, I was half way through writing something and @tom_dalling pretty much covered everything I would’ve said :slight_smile:

I think it’d be fair to consider any_instance-style RSpec code to be an indication of some sort of design smell, which is in this case accessing class constants in place rather than passing around instances of objects that are easier to replace with test doubles.

It looks like you’re trying to test something that appears in your routes? Perhaps this’d be better as an integration test anyway, where you don’t replace any parts of your code with mocks?

Anyway, this is one of the reasons in dry-web apps we combine a dry-component container with the Roda app, so we’re dealing with objects in the routes instead of concrete classes, and we keep the option (via dry-container’s stubs support) of stubbing the container for various registered objects if we really needed.

I shall add that even RSpec Core folks consider any_instance_of to be a code smell.

2 Likes