Read more

RSpec: Ensuring a method is called on an object that will be created in the future

Judith Roth
May 05, 2021Software engineer at makandra GmbH

rspec >= 3.1 brings a method and_wrap_original. It seems a bit complicated at first, but there are use cases where it helps to write precise tests. For example it allows to add expectations on objects that will only be created when your code is called.

Illustration web development

Do you need DevOps-experts?

Your development team has a full backlog? No time for infrastructure architecture? Our DevOps team is ready to support you!

  • We build reliable cloud solutions with Infrastructure as code
  • We are experts in security, Linux and databases
  • We support your dev team to perform
Read more Show archive.org snapshot

If you have older rspec, you could use expect_any_instance_of, but with the drawback, that you can't be sure if it really was the correct instance which got the message.

Example

The example model uses different validators based on a flag:

class MyModel < ApplicationRecord

  # ...

  def enhanced_valid?(skip_expensive_checks: false)
    enhanced_errors(skip_expensive_checks: skip_expensive_checks).none?
  end

  def enhanced_errors(skip_expensive_checks: false)
    validator = if skip_expensive_checks
      ActiveType.cast(self, SuperValidator)
    else
      ActiveType.cast(self, SuperDuperValidator)
    end
    validator.valid?
    validator.errors.full_messages
  end

end

The spec has to ensure that the correct Validator is used:

context 'when skipping expensive checks' do
  let(:record) { MyModel.new }

  it 'uses the SuperValidator' do
    expect(ActiveType).to receive(:cast)           # <- the method (`cast`) is mocked (replaced)
      .with(record, SuperValidator)                # <- ensure the expected arguments are given 
      .and_wrap_original do |method, *arguments|   # <- here is `and_wrap_original`
        validator = method.call(*arguments)        # <- the original method (`cast`) is explicitly called
        expect(validator).to receive(:valid?)      # <- a expectation on the then newly created object is added
        validator                                  # <- the new object is returned
      end

    record.enhanced_valid?(skip_expensive_checks: true)
  end
end

Also see here Show archive.org snapshot for further examples.

Please note: Usually it is preferrable to test behaviour, not method calls. Since the behaviour of both validators is thoroughly tested in their own specs, mocking in this case avoids unnecessary duplication.

Posted by Judith Roth to makandra dev (2021-05-05 17:28)