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.
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:
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.
In essence, the
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:
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
- It accepts
- It dynamically constructs it's subject using the passed in
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...
require 'contracts/nameable' RSpec.describe Employer, type: :model do include_examples 'nameable', :employer # ...rest of Employer tests... end
And in the Employee specs
require 'contracts/nameable' RSpec.describe Employee, type: :model do include_examples 'nameable', :employee # ...rest of Employee tests... end