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!


  1. The Shoulda DSL is actually broken up into two gems: shoulda-context and shoulda-matchers. The context gem adds subject and should syntax to Minitest unit tests, and the matchers gem brings all the ActiveRecord-specific assertions, like validate_presence_of. For Minitest you need both gems; for RSpec you only need the matchers gem. 

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

RSS
View all posts →

Open source projects

mattbrictson/nextgen

Generate your next Rails app interactively!

11
Updated 1 day ago

mattbrictson/tomo

A friendly CLI for deploying Rails apps ✨

360
Updated 1 day ago

More on GitHub →