Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails Rspec allow_any_instance_of raises "does not implement" error for ActiveRecord object attributes

I have next problem. When I try to stub instance method of ActiveRecord model with allow_any_instance_of I receive error message "Model does not implement #method", BUT if i send real request to database for create or select instance object of this model before this stub, i don't have this message and everything is ok! I have same issue in console

Reloading...

>> Search.method_defined?(:tickets_count)
=> false

>> Search.method_defined?(:affiliate_id)
=> false

>> Search.last
  Search Load (3.9ms)  SELECT  "searches".* FROM "searches"  ORDER BY "searches"."id" DESC LIMIT 1
=> #<Search:0x007fe2aecf2900
 id: 515711,
 tickets_count: 1,
 affiliate_id: nil

>> Search.method_defined?(:affiliate_id)
=> true

>> Search.method_defined?(:tickets_count)
=> true

Could anybody explain, what the hell is going on, please?))

like image 600
eugene_trebin Avatar asked Nov 10 '16 19:11

eugene_trebin


1 Answers

What you're running into here is the lazy-instantiation of the database attribute accessor methods by activerecord.

TL;DR You can use something like this https://github.com/rspec/rspec-rails/issues/1357

Essentially ActiveRecord::Base classes don't define any of the database-column based setters/getters until they need to, typically through a method-missing hook, though there may be other things that could cause them to become defined, for example calling Search.find_by_affiliate_id may also cause the definitions to load.

I can provide a high level explanation of how/why it works the way it does, but there are likely better / more up-to-date resources that can better explain it, or you can read through the ActiveRecord source code which, while a bit obtuse, can be a reasonable option for understanding its behavior.

So why aren't these methods defined?

When a new ActiveRecord class is created by inheriting from ActiveRecord::Base, all the class knows is it's expected table name, but doesn't and couldn't know which columns that table has without connecting to the database. Without knowing which columns exist, it couldn't know which readers/writers to define. ActiveRecord does not proactively query the database at load time, which I think is a very good thing, as it lets you load your code without requiring a migrated, connected database.

What ActiveRecord does is hook into method_missing, responds_to?, etc. to determine when a method is being called on an ActiveRecord class that is not defined, attempting to load the current db schema and define reader/writer/accessor methods on itself for each column it finds, then retrying the method call to see if it's now defined.

In your example above, it's likely not the .last call that is defining the attributes, but rather the .inspect call that your terminal runs in order to print the instance of the Search object.

To test this, you could re-ran your example above but replace Search.last with (s = Search.last).nil?. If you did that instead, I would bet that Search.method_defined?(:affiliate_id) would still be false.

Hope that explains why the methods were not defined at first, but were later on. This is also the reason why you couldn't use alias_method :aliased_affiliate_id, :affiliate_id, which would fail because there's no method affiliat_id defined at load time.

For what you're trying to do, you need to decide if it's worth it to require an active database connection in order to run these specs.

If so, you can trigger your ActiveRecord classes to load their table definitions and define their reader/writer attributes in your specs in order to make this pass.

Here's someone running into the same issue with rspec with a monkey-patch solution that loads the definitions for activerecord classes:

https://github.com/rspec/rspec-rails/issues/1357

like image 90
melcher Avatar answered Sep 24 '22 22:09

melcher