RSpec is great for writing tests in ruby
, providing a nice DSL for testing expected behaviour. However, as a test suite grows, you may find yourself writing a lot of boiler plate and even duplicating specs. RSpec’s shared_examples_for
can be a good way to DRY up some of these cases. I couldn’t find much documentation on using it, so wrote a short about it.
Introduction
I generally strive to use TDD/BDD when writing software, and for this RSpec is my library of choice. It’s got a nice methodology for writing tests in a logical manner, resulting in easy to follow and easy to read specs:
- describe what you’re doing
- establish a context
- test some expected behavior
When writing application code, you to keep your code DRY by pulling out common functionality into modules and including the module rather than rewriting the common code. This is something you can do with your specs as well.
Duplicated specs
Let’s say our application has User
and Project
models and both of these items can be ‘followed’. With the ‘follow’ logic extracted out into a Followable
module our models might look something like this:
# app/models/concerns/followable.rb
module Followable
extend ActiveSupport::Concern
included do
has_many :followings, as: :followable, dependent: :destroy, class_name: 'Follow'
has_many :followers, through: :followings, source: :user
end
def add_follower(user)
followings.create(user: user)
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Followable
end
# app/models/project.rb
class Project < ActiveRecord::Base
include Followable
end
Using Factory Girl to set up our test data, our basic specs might look something like this:
# spec/models/user_spec.rb
require 'rails_helper'
describe User do
let(:user) { create(:user) }
describe '#add_follower' do
it 'should add a new follower' do
expect { user.add_follower }.
to change { user.followers.count }.from(0).to(1)
end
end
end
# spec/models/user_spec.rb
describe Project do
let(:project) { create(:project) }
describe '#add_follower' do
it 'should add a new follower' do
expect { project.add_follower }.
to change { project.followers.count }.from(0).to(1)
end
end
end
While our Followable
module keeps our model code nice and DRY, we still want to test that the ‘follow’ functionality works properly for both Users and Projects - we end up with duplication in our specs.
Removing the duplication
Instead, we can pull the duplicated spec out into a reusable block using shared_examples_for
:
# spec/spec_helper.rb
Dir[Rails.root.join('spec/shared_examples/**/*.rb')].each { |f| require f }
# spec/shared_examples/followable.rb
shared_examples_for 'is_followable' do
let(:resource) { create(described_class.name.underscore) }
describe '#add_follower' do
it 'should add a new follower' do
expect { resource.add_follower }.
to change { resource.followers.count }.from(0).to(1)
end
end
end
Then we can call it in our User
and Project
specs and call it using it_behaves_like
.
require 'rails_helper'
# spec/models/user_spec.rb
describe User do
it_behaves_like 'is_followable'
end
# spec/models/project_spec.rb
describe Project do
it_behaves_like 'is_followable'
end
The shared example has access to the described_class
, which is the class being currently tested. It is passed in form the initial ‘describe User do...
’ at the start of the spec. From that it’s easy to determine the factory name, and create a factory instance on which to perform our tests.
Conclusion
Our tests are much cleaner and can easily be extended if other resources become followable. The Followable
module may contain a lot more logic we need to test and if we decide at a later date that another resource is to be followable, it just takes one line of code to add tests for that.
The examples above are using rails ActiveRecored
models, but this is by no means a rails-specific, any ruby tests using RSpec can make use of shared_examples_for
.
Hope this helps.