rspec-rails: Dynamic method verification fails when method stubbed using allow_any_instance_of on a class not previously instantiated.

Dynamic method verification fails when method stubbed using allow_any_instance_of on a class not previously instantiated.

Given a test stubs a method on a class using allow_any_instance_of, and verify_partial_doubles is enabled, rspec-rails will fail to resolve the dynamic method if it is stubbed via any_instance prior to any instance(s) of that class being instantiated.

Environment

  • ruby 2.1.5
  • rails (3.2.19)
  • rspec-core (3.2.3)
  • rspec-expectations (3.2.1)
  • rspec-mocks (3.2.1)
  • rspec-rails (3.2.1)
  • rspec-support (3.2.2)

Steps To Reproduce:

  • Create a rails app
  • Generate a model with some fields: rails g model Article title:string text:text
  • Add the following test:
require 'rails_helper'

RSpec.describe 'dynamic methods' do
  it 'does not resolve dynamic instance methods on first call to any_instance' do
    allow_any_instance_of(Article).to receive(:title).and_return 'foo'
    a = Article.new
    expect(a.title).to eql 'foo'
  end
end
  • Migrate and run the test. It will fail with the message:
RSpec::Mocks::MockExpectationError: Article does not implement #title
./spec/models/any_instance_spec.rb:5:in `block (2 levels) in <top (required)>'
-e:1:in `load'
-e:1:in `<main>'
  • Change Article in the stub to Article.new.class:
  it 'does not resolve dynamic instance methods on first call to any_instance' do
    allow_any_instance_of(Article.new.class).to receive(:title).and_return 'foo'
    a = Article.new
    expect(a.title).to eql 'foo'
  end
  • Run the test again. It will pass.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 7
  • Comments: 27 (16 by maintainers)

Most upvoted comments

Model.define_attribute_methods would force rails to load methods without doing any db queries

As a data point: this bit me today during a project to enable partial double verification on an existing Rails codebase. Naturally a future project will be to eliminate uses of allow_any_instance_of, but I want to finish the current job first.

For now I’m going to edit the failing example to add Article.define_attribute_methods before the use of allow_any_instance_of(Article) and move on.

I believe this has to do with how activerecord works – the dynamic column methods are not initially defined, so that Article.method_defined?(:title) lies and returns false. Our docs mention this gotcha:

https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/verifying-doubles/dynamic-classes

However, @JonRowe added an improvement in #1238 that addresses this for instance_double. @JonRowe, do you think you can leverage that solution for this case, too?

@00dav00 AFAIK ActiveResource is no longer part of Rails and is an external gem, as such no configuration for it belongs in rspec-rails, you could of course release a gem for adding this, the config would be roughly:

::RSpec::Mocks.configuration.when_declaring_verifying_double do |possible_model|
  target = possible_model.target

  # I am assuming active resource does not have `abstract_class?` like active record does
  if Class === target && ActiveResource::Base > target
    # Something that defines methods based on a schema
  end
end

To be included in a railtie or other configuration.

Of course that assumes that Active Resource knows about its methods like Active Record does… The implementation for ActiveRecord works because it’s just lazy, the hook triggers a schema load (I think) which defines all the attributes you’d expect.

Closed by rspec/rspec-mocks#1309

This problem also hit me today.

I noticed that the test failed only if executed in isolation. So i decided to force the “loading” of the class by myself.

If you do encounter this kinda of problem with an ActiveRecord model, you should be able to do the following:

Model.create(args) # In my case I use FactoryBot for this
Model.all.first.method # Force rails to load methods
allow_any_instance_of(Model).to receive(:method)

My test is now passing. If I’m wrong in any point, please let me know. 😃