Dimitry_N Dimitry_N - 1 year ago 37
Ruby Question

Metaprogramming predicate methods inside a Class - Rails 4

I have a class called

, which
has_many: roles, through: user_roles
. I am trying to metaprogram predicate methods for associated roles inside a user class like so:

class User < ActiverRecord::Base
Role.all.pluck(:name).each do |role_name|
define_method("#{role_name}?") do

does return an array of existing role names, the
never gets called, and my spec fails with undefined method:

subject.roles << create(:role, name: 'foo')
expect(subject.foo?).to be true #<= undefined method `foo?' for #<User...>

Answer Source

The metaprogramming technique you're employing is meant to take advantage of the fact that code just sitting inside of a class is going to be executed when the class is loaded. So when Rails loads your User class, at that moment the logic to define the predicate methods is being executed and methods are being created for whatever Role.all returns at that moment when User is loaded.

Creating a new role, as you do in the test, will therefore have no effect on what predicate methods were created when the class was loaded and the code was executed.

You can see this in action by creating a file called count.rb in any directory with the following code inside:

$count += 1

Then, open irb and type:

irb(main):001:0> $count = 0
=> 0
irb(main):002:0> require './count'
=> true
irb(main):003:0> $count
=> 1

Notice that $count was incremented by one when the file was loaded. Now, if you were to require the file again, nothing would happen. You could force the code to be re-loaded by using load instead of require:

# ...continued from above
irb(main):004:0> require './count'
=> false
irb(main):005:0> $count
=> 1
irb(main):006:0> load './foo.rb'
=> true
irb(main):007:0> $count
=> 2

So to get your test to pass, you would have to create the role, then force a reload of the User class, then make the assertion.