Shoulda Matchers 2.6.0

Elliot Winkler

For the past four months, we’ve been hard at work on the next version of shoulda-matchers, and we’re pleased to announce it’s up on RubyGems.

2.6.0 is first and foremost a compatibility release, bringing a few of the matchers up to date with the newest changes in Rails 4.1. However, there are also some new matchers for you to use.

The following is a brief highlight of the most important changes; there are plenty of additional fixes and additions we made, so read the NEWS file for the full scoop.

Matchers restored from 2.0.0

If you remember, when we released 2.0.0, we decided to remove a couple of matchers as we hadn’t quite polished them enough. They’re now back!

You can now use the permit matcher for testing usage of strong parameters in your controller:

class UserController < ActionController::Base
  def create
    User.create(user_params)
  end

  private

  def user_params
    params.require(:user).permit(:email)
  end
end

# RSpec
describe UserController do
  it { should permit(:email).for(:create) }
end

# Test::Unit
class UserControllerTest < ActionController::TestCase
  should permit(:email).for(:create)
end

You can also use delegate_method for usage of Rails’s delegate method or other forms of delegation:

class Human < ActiveRecord::Base
  has_one :robot
  delegate :work, to: :robot

  # alternatively, if you are not using Rails
  def work
    robot.work
  end

  def protect
    robot.protect('Sarah Connor')
  end

  def speak
    robot.beep_boop
  end
end

# RSpec
describe Human do
  it { should delegate_method(:work).to(:robot) }
  it { should delegate_method(:protect).to(:robot).with_arguments('Sarah Connor') }
  it { should delegate_method(:beep_boop).to(:robot).as(:speak) }
end

# Test::Unit
class HumanTest < ActiveSupport::TestCase
  should delegate_method(:work).to(:robot)
  should delegate_method(:protect).to(:robot).with_arguments('Sarah Connor')
  should delegate_method(:beep_boop).to(:robot).as(:speak)
end

Callback matchers

There are also new matchers for testing callbacks in controllers:

  • use_before_filter
  • use_after_filter
  • use_around_filter

These are aliased as use_<callback>_action, for consistency with the renamed callback macros in Rails 4. Here’s an example:

class UserController < ActionController::Base
  before_filter :authenticate_user!
end

# RSpec
describe UserController do
  it { should use_before_filter(:authenticate_user!) }
end

# Test::Unit
class UserControllerTest < ActionController::TestCase
  should use_before_filter(:authenticate_user!)
end

validatepresenceof + hassecurepassword could raise an error

We received a report that under Rails 4.1, validate_presence_of could fail when used against a record with an already set password which is part of a model that has_secure_password:

class User < ActiveRecord::Base
  has_secure_password validations: false
  validate_presence_of :password
end

describe User do
  it do
    user = User.new(password: 'whatever')

    # This would fail with a message like:
    # Expected "can't be blank" on password, got no errors
    user.should validate_presence_of(:password)
  end
end

Why does this fail?

validate_presence_of works by setting the attribute in question to nil, calling #valid? on the record, and then asserting that an “is blank” error message appears on that attribute.

In Rails 4.1, has_secure_password was changed so that password cannot be set to nil. Therefore, if, in your test, password is already set to a value before validate_presence_of is called, password will never get unset. Therefore it is impossible to test that errors occur when the validation is violated, so using validate_presence_of will now fail to match. In order to highlight why the match fails, we capture the failure as a CouldNotSetPasswordError exception:

describe User do
  it do
    user = User.new(password: 'whatever')

    # This now raises an exception:
    # Shoulda::Matchers::ActiveModel::CouldNotSetPasswordError
    user.should validate_presence_of(:password)
  end
end

What can you do to fix this? Call validate_presence_of on an empty record instead:

describe User do
  it do
    user = User.new
    user.should validate_presence_of(:password)
  end
end

allow_value could raise an error

While we investigated the issue referred to above, we realized it revealed a larger issue. The allow_value matcher also takes the same approach as validate_presence_of, only more generically. That is, it works by taking the value you provide, setting it on the attribute in question, running validations, and then asserting that there are no errors on that attribute.

This means that it’s also subject to the same problem as validate_presence_of: if the attribute somehow prevents the value from being set, this will interfere with the assertion that allow_value is making, because now the test will be using a different value than you specified. In this case, we raise an CouldNotClearAttribute exception.

This change could affect you particularly if you are using should_not allow_value. For example:

class Issue < ActiveRecord::Base
  validates_presence_of :status

  def status=(value)
    super(value || 'created')
  end
end

describe Issue do
  it { should_not allow_value(nil).for(:status) }
end

Prior to 2.6.0, this test would have passed. While the test as written is technically correct, the test as executed is not. Setting status to nil actually results in status being set to "created". Therefore, the test above is actually equivalent to the following:

describe Issue do
  it { should_not allow_value('created').for(:status) }
end

Obviously, this is fundamentally a different test than the above. Should this new test pass? In this case, it does. But since allow_value is generic, it depends on whatever validation you’ve placed on your attribute. We don’t know what that is from the perspective of the matcher, so in 2.6.0, we raise an exception and let you specify more of what you want.

What can you do about this? Again, it depends on validations you may have present. In our example above, we actually don’t need the presence validation since it will never fail, and that means we can get rid of our allow_value test. You may have to take a different approach in order to resolve this.

ensureinclusionof now works with boolean columns (with caveats)

This issue has evaded us for a while. The fix was not straightforward and we ultimately ended up making a couple of compromises.

One case the fix addressed was testing that a boolean column accepts true or false or both. If you say that a boolean column only accepts true, you have to test that it can’t accept false; if you specify that it only accepts false, you have to test that it can’t accept true. That part’s easy:

class Issue < ActiveRecord::Base
  validates_inclusion_of :open, in: [true]
  validates_inclusion_of :closed, in: [false]
end

describe Issue do
  it { should ensure_inclusion_of(:open).in_array([true])
  it { should ensure_inclusion_of(:false).in_array([false])
end

But what if a column can accept both true and false? What do you test? Well, there’s no value that a boolean column can take other than true and false, so this sort of test is impossible to write:

class Issue < ActiveRecord::Base
  validates_inclusion_of :open, in: [true, false]
end

describe Issue do
  # This will work, but will give you a warning
  it { should ensure_inclusion_of(:open).in_array([true, false])
end

The other case addressed by this fix occurs if you have a boolean column which is non-nullable and you test that the column should accept nil as a value. This is also an impossible test because a non-nullable column, by definition, cannot accept nil; and because it’s a boolean column in this case, as soon as you set it to nil it will get coerced to false.

create_table :issues do |t|
  t.boolean :open, null: false
end

describe Issue do
  # This will also work, but will also give you a warning
  it { should_not ensure_inclusion_of(:open).in_array([nil])
end

Other notable features and bugfixes

  • Association matchers gained the autosave option.
  • validate_numericality_of gained the allow_nil option.
  • belong_to gained the inverse_of option.
  • rescue_from now works with exception handler methods marked as protected or private.

Thank you

I’d like to thank the contributors that helped us with this release:

Enjoy!