ActiveRecord Strict Validations, Minitest, and Shoulda
Are you using thoughtbot’s Shoulda gems with Minitest? How about strict validations in ActiveRecord? Here’s why I think these are great things to add to your Rails backpack of tools, and how to set them up.
Shoulda + Minitest is great
One of my first tasks when building a new feature in a Rails app is setting up models and validations. For example, I might define a trivial user model like this:
# app/models/user.rb
class User < ActiveRecord::Base
validates_presence_of :email
validates_format_of :email, :with => /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
end
shoulda-context and shoulda-matchers provide a simple DSL that makes testing validations like that very easy:
# Gemfile
group :test do
gem "shoulda-context"
gem "shoulda-matchers", ">= 3.0.1"
end
# test/unit/user_test.rb
require "test_helper"
class UserTest < ActiveSupport::TestCase
should validate_presence_of(:email)
should allow_value("hello@mattbrictson.com").for(:email)
should_not allow_value("@hello").for(:email)
end
Behind the scenes, Shoulda1 is building an instance of User
(what it calls the “subject” of the test), and then running the appropriate assertions to verify that the validations are working. This is great because Shoulda is eliminating the boilerplate for an incredibly common style of unit test.
Enter strict validations
Have you ever used ActiveRecord’s strict
option when declaring a validation rule? It looks like this:
# app/models/user.rb
class User < ActiveRecord::Base
belongs_to :invited_by, :class_name => "User"
validates_presence_of :invited_by, :strict => true
end
What I’ve done in the above example is add an invited_by
association. The presence validator says that this value must be present. Furthermore, because of :strict => true
, this validator will raise an exception if validation fails.
# Without :strict => true
User.new.valid?
# => false
# With :strict => true
User.new.valid?
# => ActiveModel::StrictValidationFailed: Invited by can't be blank
Why use strict?
Why would I want to raise an exception for a presence validation? It comes down to whether or not the validation rule is checking user input.
In this case, I don’t intend for the user to provide the invited_by
value. It isn’t even a field in the UI. Rather, it is the controller’s responsibility to fill it in, perhaps like this:
class UsersController < ApplicationController
def create
@user = User.new(:invited_by => current_user)
if @user.update(user_params)
# ...
else
render("new")
end
end
end
Normally, if validation fails, the form is re-rendered (i.e. render "new"
) and errors are displayed, guiding the user to correct their input. But for a non-UI based field like invited_by
, the user can’t do anything to correct the problem. The normal validation flow doesn’t make sense for this type of attribute.
Hence the :strict => true
option. The :strict
option makes it clear to developers what the intention is for the invited_by
field. Essentially it is documenting the API of the User model. By raising an exception, it indicates that the developer has made a mistake using this API; it’s not due to user input.
I make heavy use of form objects in my Rails apps, and :strict
comes in very handy for checking that collaborating objects have been properly set up. It’s a great example of a well-designed DSL: readable, concise, and self-documenting.
Testing strict with Shoulda and Minitest
The problem with strict validations is that it throws a wrench into our simple Shoulda-based unit tests. The existence of a strict validation makes the other validations fail. My first instinct, after reading up on Shoulda’s strict
support, was to write my test like this:
# test/unit/user_test.rb
require "test_helper"
class UserTest < ActiveSupport::TestCase
should validate_presence_of(:email)
should validate_presence_of(:invited_by).strict
end
But, here’s what happens:
ERROR["test_: User should require email to be set. ", UserTest, 0.903375]
test_: User should require email to be set. #UserTest (0.90s)
ActiveModel::StrictValidationFailed: Invited by can't be blank
When Shoulda is testing the presence validator for email
, it fails because an exception is raised by the strict validator for invited_by
. Why is this?
Behind the scenes, Shoulda is running its tests against a test “subject”, which it inferred to be User.new
. But a new user doesn’t have the invited_by
value filled in yet. So any call to valid?
will raise StrictValidationFailed
.
The solution is to give Shoulda a different subject.
# test/unit/user_test.rb
require "test_helper"
class UserTest < ActiveSupport::TestCase
subject { User.new(:invited_by => User.new) }
should validate_presence_of(:email)
should validate_presence_of(:invited_by).strict
end
That’s it! Now Shoulda will use a user object that already has invited_by
filled in. The tests now pass.
Conclusion
I really like using strict validations to document the API/collaborator requirements (as opposed to user input requirements) of my ActiveRecord models and form objects.
Testing these validation rules is simple, as long as I remember to use subject
to set up the necessary prerequisites. Then I can use the nice should
syntax to write all my validation tests with expressive one-liners.
Are you using strict validations? How do you test them? Let me hear about it!
-
The Shoulda DSL is actually broken up into two gems:
shoulda-context
andshoulda-matchers
. The context gem addssubject
andshould
syntax to Minitest unit tests, and the matchers gem brings all the ActiveRecord-specific assertions, likevalidate_presence_of
. For Minitest you need both gems; for RSpec you only need the matchers gem. ↩
You just read
ActiveRecord Strict Validations, Minitest, and Shoulda
Are you using thoughtbot’s Shoulda gems with Minitest? How about strict validations in ActiveRecord? Here’s why I think these are great things to add to your Rails backpack of tools, and how to set them up.
Share this post? Copy link
About the author
Hi! I’m a Ruby and CSS enthusiast, regular open source contributor, software engineer, and occasional blogger writing from the San Francisco Bay Area. Thanks for stopping by! —Matt