Voodoo send

July 12th, 2008

Recently when looking through some plugin’s code, we found the following line:

self.send(:include, Foo)

This seems to be an acceptable variant of the commonly used

ActiveRecord::Base.send(:include, Foo)

As you may have surmised, it is not an acceptable variant. This is a Voodoo programming smell, as it indicates a misunderstanding of exactly what the semantics of send are. There is also a clearer, more intention revealing, and identical1 alternative:

include Foo

or with the omitted parenthesis:

include(Foo)

This of course, leads us to why ActiveRecord::Base.send(:include, Foo) is acceptable. The include method is private, so the most elegant way to invoke it on another object is by using send; this is possible because send ignores access control allowing us to invoke include directly.

1 Almost, the difference is that send is in the call stack.

RSpec 1.1.4 and Helpers

July 1st, 2008

Last week, RSpec 1.1.4 was released; and given my optimistic disposition I immediately upgraded. Happily, the upgrade was almost seamless. Two interdependent issues kept this from being a flawless upgrade:

  1. A new deprecation warning on our helper specs (no one really likes to see deprecation warnings)
  2. A bug in the new way to write helper specs (a show stopper)

Lets see what happens when we run our spec1.

Wonderland$ spec spec/helpers/dog_helper_spec.rb 
Modules will no longer be automatically included in RSpec version 1.1.4.  Called from ./spec/helpers/dog_helper_spec.rb:8
.Modules will no longer be automatically included in RSpec version 1.1.4.  Called from ./spec/helpers/dog_helper_spec.rb:16
.
Finished in 0.393129 seconds
2 examples, 0 failures

Lets examine the helper spec that generated these warnings:

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe DogHelper do

  describe "name_or_description()" do
    it "should return a description for a dog without a name" do
      dog = mock_model(Dog, :name => nil, :sex => 'male', :breed => 'Labrador', :color => 'black')
      name_or_description(dog).should == "(male Labrador, black)"
    end
  end

  describe "owner_link()" do
    it "should return a link to the owner" do
      owner = mock_model(User, :login => 'sally_smith', :to_param => 'sally_smith')
      dog = mock_model(Dog, :owner => owner)
      owner_link(dog).should =~ /users\/sally_smith/
    end
  end
end

So, what does this deprecation warning tell us? We are being warned that calls to name_or_description and owner_link, which are called on the implicit self of the RSpec it blocks, are going to fail in the future when the helper module (DogHelper in this example) is no longer automatically included by RSpec.

Or put another way, calling helper methods like name_or_description directly, exactly as one might in a view, will no longer work.

The new way to call helper methods is through the helper attribute of RSpec, giving access to an ActionView::Base instance with the helper module included. Lets go ahead and make this change to our two helper calls:

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe DogHelper do

  describe "name_or_description()" do
    it "should return a description for a dog without a name" do
      dog = mock_model(Dog, :name => nil, :sex => 'male', :breed => 'Labrador', :color => 'black')
      helper.name_or_description(dog).should == "(male Labrador, black)"
    end
  end

  describe "owner_link()" do
    it "should return a link to the owner" do
      owner = mock_model(User, :login => 'sally_smith', :to_param => 'sally_smith')
      dog = mock_model(Dog, :owner => owner)
      helper.owner_link(dog).should =~ /users\/sally_smith/
    end
  end
end

Lets try this out!

Wonderland$ spec spec/helpers/dog_helper_spec.rb 
.F
1)
NoMethodError in 'DogHelper owner_link() should return a link to the owner'
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.url_for
(eval):17:in `user_path'
/Users/leshill/src/rails/rspec114/app/helpers/dog_helper.rb:9:in `owner_link'
./spec/helpers/dog_helper_spec.rb:16:
Finished in 0.398686 seconds
2 examples, 1 failure

Hmmm, we have gotten rid of the deprecation warnings, but we now have a new (and truly unexpected!) failure.

This is a bona-fide bug in 1.1.4, and might lead to our having to rollback to 1.1.3 if there was no easy workaround. Thankfully, there is a fix already available for 1.1.5 , and that fix can be easily adapted within our own specs.

Lets add this before block to our spec:

before(:each) do
    # Patch until 1.1.5
    helper_controller = @controller
    helper.instance_eval { @controller = helper_controller }
  end

And then try it out:

Wonderland$ spec spec/helpers/dog_helper_spec.rb 
..
Finished in 0.40663 seconds
2 examples, 0 failures

Much better!

1 I am using a mock spec to demonstrate the issue, the minimal Rails app which fits around this spec is left as an exercise for the reader.