— Tagged with: , , — Written by

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.