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 theallow_nil
option.belong_to
gained theinverse_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:
- Mauro George for your work on Rails 4.1 support
- Fellow tb'ers Damian, Harry, Mason and
Anthony for adding
permit
,delegate_method
, the callback matchers, and for fixingvalidates_uniqueness_of
so it works alongsidehas_secure_password
- Yukio Mizuta, Chulki Lee, Andy Chambers, Daniel Morris, Dmitry Tonkonogov, petedmarsh, and pikachuEXE for features and bugfixes
Enjoy!