An Introduction to Contract Testing in Rails

Greater confidence. Fewer specs.

After years of listening to J.B. Rainsberger tell me Contract tests are great, I finally started listening.

I always assumed they were confusing or hard for some reason. Turns out I was really wrong. To help you along I've written this short tutorial on how to write Contract Tests in Rails with RSPec.

Duck Typing

One of the great things about dynamic languages is Duck Typing. This allows us to define methods that aren't fussy about the type of objects they're given. As long as it has the methods we need and they work roughly how we expect, we'll take it!

A Problem with Duck Typing

How can we be really confident that our objects behave correctly?

You could write two separate test suites that essentially have duplicate tests. Unfortunately, as time passes those test suites will almost certainly drift apart. As your team grows, and more objects are added that should fit the contract, the likelihood of forgetting to sync changes rises.

Instead we can write a Contract.

What is a Contract?

A Contract is simply a description of how to work with an object.

In theory, Contracts are similar to Interfaces. Like an Interface, a Contract tells you what methods an object should have. But unlike an Interface, a Contract also tells you how an object should behave.

In practice, Contracts are shareable test suites. They are written to test any type of object. By adding them to your object's test suite you can ensure that it properly adheres to the contract. Better yet, it will be fully covered by tests without having to write any duplicate test code.

Example: Writing Contracts in RSPec

RSpec has a feature called shared_examples which lets you include tests into any test suite. We'll look at a real example of a Contract test I added to the codebase at work.

At HeartPayroll (HP) we have two kinds of accounts: Employers and Employees. These are separate tables, with a lot of difference, but they do have some things in common. For example, they both have first and last names and convenience methods for displaying them (e.g. account.full_name, account.last_name_first_name)

In essence, the Employee and Employer classes share a "Contract". Anytime we're looking to display information about an account, we expect to be given either class.

To ensure these classes behave as expected, I created a new contract in the spec folder:

spec/contracts/nameable.rb

RSpec.shared_examples 'nameable' do |nameable_factory|
  describe '#full_name' do
    it 'returns full name' do
      nameable = FactoryBot.build(nameable_factory)

      expect(nameable.full_name).to eq([nameable.first_name, nameable.last_name].compact.join(' ').strip)
    end
  end
end

This spec file is a bit different than the usual spec.

  • It uses RSpec.shared_examples not RSpec.describe
  • It accepts nameable_factory
  • It dynamically constructs it's subject using the passed in nameable_factory

This is effectively an Abstract Test Suite. It can run on any type of object to ensure it adheres to the "Nameable" Contract.

To include it with our specs, we can use the include_examples method in both our Employer specs...

spec/models/employer.rb

require 'contracts/nameable'

RSpec.describe Employer, type: :model do
  include_examples 'nameable', :employer

  # ...rest of Employer tests...
end

And in the Employee specs

spec/models/employee.rb

require 'contracts/nameable'

RSpec.describe Employee, type: :model do
  include_examples 'nameable', :employee

  # ...rest of Employee tests...
end