Testing Context Validations - DockYard

checklist
Dan McClain

Partner & Developer

Dan McClain

Quick Refresher on ContextValidation

A few months ago, Brian released the ContextValidations gem. ContextValidations moves your model validations to the controller, allowing you to vary your validations by context, rather than relying on conditional validations.

Let’s validate our user

We have a user model, that requires a password and a username when a user signs up. They can change their username and password, but if they can leave the password blank when updating their account, it will retain the old password. Whenever they enter a password , it must be 9 characters or greater. We’re going to ignore the actual implementation of the password saving scheme and password confirmation in this example. Also, this example ignores setting up the test helper for valid_attribute and MiniTest::Spec.

Implementing the Tests and Validations in the Model

To test the above requirements model validations, we’d do the following:

describe OldUser do
  describe 'new user' do
    subject { OldUser.new password: 'password_to_confirm' }

    it { must have_valid(:username).when('bob', 'test1234') }
    it { wont have_valid(:username).when('', nil) }
    it { must have_valid(:password).when('validpassword1234') }
    it { wont have_valid(:password).when('', nil, 'tooshort') }
  end

  describe 'existing user' do
    subject { old_users(:example) }

    it { must have_valid(:username).when('bob', 'test1234') }
    it { wont have_valid(:username).when('', nil) }
    it { must have_valid(:password).when('', nil, 'validpassword1234') }
    it { wont have_valid(:password).when('tooshort') }
  end
end

And here is the implementation of the model:

class OldUser < ActiveRecord::Base
  attr_accessor :password
  validates :username, presence: true
  validates :password, presence: true, if: :new_record?
  validates :password, length: { minimum: 9 }, allow_blank: true
end

Implementing the Tests and Validations in the Controller with ContextValidations

We’ve been using ContextValidations with our client work since its release and realized we could unit test the controller to test the validations.

Our unit tests for the controller are here:

describe UsersController do
  describe '#create' do
    subject { User.new(validations: validations_for(:create)) }

    it { must have_valid(:username).when('bob', 'test1234') }
    it { wont have_valid(:username).when('', nil) }
    it { must have_valid(:password).when('validpassword1234') }
    it { wont have_valid(:password).when('', nil, 'tooshort') }
  end

  describe '#update' do
    subject { User.new(validations: validations_for(:update)) }

    it { must have_valid(:username).when('bob', 'test1234') }
    it { wont have_valid(:username).when('', nil) }
    it { must have_valid(:password).when('', nil, 'validpassword1234') }
    it { wont have_valid(:password).when('tooshort') }
  end
end

Note the use of validations_for. It is a MiniTest helper method defined by ContextValidations, which looks up the name of the controller from the describe block, creates an instance of it, and retrieves the validations for the context passed in. This prevents you from needing to create your own instance and calling validations on it. The resulting tests end up looking very similar to what your model tests would look like.

Our model implementation is very light:

class User < ActiveRecord::Base
  include ContextValidations::Model

  attr_accessor :password
end

And our validations are defined in the controller:

class UsersController < ApplicationController
  include ContextValidations::Controller

  private

  def base_validations
    validates :username, presence: true
    validates :password, length: { minimum: 9 }, allow_blank: true
  end

  def create_validations
    validates :password, presence: true
  end
end

All of the examples are part of this repository.

Wrapping it up

As you can see, writing the validation tests for the controller are almost identical to writing them for the model. There are a few differences in setting up the subject for the tests, but the only major difference is that you are testing the controller instead of the model. If you have any feedback on the tests we came up with, feel free to let us know!

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box