Sometimes, when testing your code with RSpec, you’ll notice similarities and duplication between your spec files. Most of these will involve setup that doesn’t say much about the object under test. There’s something that helps you to reduce this duplication: Custom example groups! RSpec itself (rspec-rails) uses example groups for the different types of tests for models, controllers, helpers and views. In the following I’ll show you how to use them to reduce duplication and improve your tests.
What are example groups?
An example group is defined by using RSpec’s describe
method (or its alias context
). It’s a subclass of RSpec::Core::ExampleGroup
and provides all the macro methods you’re used to in your specs. Additionally it provides a description, a set of metadata, and optionally a described class. It also knows about the examples defined using it
and how to run each of them with all hooks and stuff.
Customizing example groups
Now that you know about example groups, let’s start customizing them. As we’ve learned above, example groups are simple classes and therefore can be extended with modules. This happens to be the way rspec-rails is working as well. To illustrate the process, let’s look at an example.
A serializer example group
In a recent project, I was working with ActiveModel::Serializers
. In case you don’t know it, you should definitely have a look at it. In my opinion, it’s a great way to serialize your models into JSON. For our example we want to create a custom example group for serializers so we can test them like this:
require 'spec_helper'
describe UserSerializer do
let(:attributes) { FactoryGirl.attributes_for(resource_name) }
it { should have_key(:name) }
it { should have_key(:email) }
it { should have_key(:created_at) }
it { should have_key(:updated_at) }
end
In order to get there we create a new file called spec/support/example_groups/serializer_example_group.rb
. Then we define a new module called SerializerExampleGroup
. For convenience (and following the pattern of rspec-rails) we extend this module with ActiveSupport::Concern
. In addition we must tell RSpec to include this module into the example groups of all specs in the spec/serializers
folder and all specs explicitly setting :type => :serializer
.
module SerializerExampleGroup
extend ActiveSupport::Concern
RSpec.configure do |config|
config.include self,
:type => :serializer,
:example_group => { :file_path => %r(spec/serializers) }
end
end
Adding custom behavior
That’s all we have to do to set up our custom example group. Next we add our desired custom behavior. We have to create a new instance of our serializer and pass a resource to serialize. The resource needs attributes and must implement a read_attributes_for_serialization
method. We can add all this by defining an included
block and use RSpec’s let
macro. In order to get the name of the resource that is serialized by the serializer under test we use RSpec’s described_class
method. It returns the class defined in the describe UserSerializer
statement. By converting its name to underscores, removing the _serializer
part and converting it into a symbol we get :user
as resource_name
.
included do
let(:attributes) do
{}
end
let(:resource_name) do
described_class.name.underscore[0..-12].to_sym
end
let(:resource) do
double(resource_name, attributes).tap do |double|
double.stub(:read_attribute_for_serialization) { |name| attributes[name] }
end
end
end
Next we set the serialized hash as the subject for the example group. For convenience we also convert it into a HashWithIndifferentAccess
so we don’t have to think about using strings or symbols as keys.
let(:serializer) { described_class.new(resource) }
subject { serializer.serializable_hash.with_indifferent_access }
Adding some metadata
Now we have the desired custom behavior. To wrap things up, we add some metadata to the example group. In the included
block we have access to the metadata
method and are able to set the :type
to :serializer
.
metadata[:type] = :serializer
The finished example group
All this results in a spec/support/example_groups/serializer_example_group.rb
file looking like this:
module SerializerExampleGroup
extend ActiveSupport::Concern
included do
metadata[:type] = :serializer
let(:attributes) do
{}
end
let(:resource_name) do
described_class.name.underscore[0..-12].to_sym
end
let(:resource) do
double(resource_name, attributes).tap do |double|
double.stub(:read_attribute_for_serialization) { |name| attributes[name] }
end
end
let(:serializer) { described_class.new(resource) }
subject { serializer.serializable_hash.with_indifferent_access }
end
RSpec.configure do |config|
config.include self,
:type => :serializer,
:example_group => { :file_path => %r(spec/serializers) }
end
end
With this custom example group in place, testing the serializers gets much easier. Of course, this is only one example of what you can do. Instead of adding a lot of setup you might only use this to add metadata (such as tags) to specs in a specific folder.