RSpec Subject, Helpers, Hooks and Exception Handling

After understanding the basic structure how tests look with RSpec, the next step is to learn how to use the most frequently used elements of its DSL.

Brought to you by

Semaphore

We are continuing an introduction to RSpec, a testing tool for Ruby. After covering the very basics in the first part of this series, we will now explore other important and frequently used features of RSpec: use of subject, let helper method, before hooks and exception handling. Along the way we will encounter some more matchers and how to compose them.

After this tutorial you should have a solid background to start using RSpec in your Ruby projects and apply its features in an idiomatic way.

Test Subjects

Let's say that we need to write a simple program for runners who need to log their runs and are interested in seeing some weekly statistics. Basic information about a run includes distance, duration and when it happened.

We know that there should be a class Run with three attributes, which we should be able to initialize easily. We may start by describing it as follows:

describe Run do

  describe "attributes" do

    subject do
      Run.new(:duration => 32,
              :distance => 5.2,
              :timestamp => "2014-12-22 20:30")
    end

    it { is_expected.to respond_to(:duration) }
    it { is_expected.to respond_to(:distance) }
    it { is_expected.to respond_to(:timestamp) }
  end
end

In this example we declare a subject to be an instance of class Run. The reason why we define it is that we have multiple test examples that work with the same test subject. RSpec understands it as an object which should respond to (in core Ruby sense) a number of methods, such as duration. The expectation is using RSpec's built-in respond_to matcher.

The one-line syntax shown above is convenient when you can avoid duplication between a matcher and the string that documents the test example. If this were not possible, we would need to write the examples above like this:

it "responds to '#duration'" do
  expect(subject).to respond_to(:duration)
end

In fact, if we our objects can be initialized without parameters, we can make it even shorter:

describe Run do
  it { is_expected.to respond_to(:duration) }
end

This is because we passed the class to the describe block and RSpec has already initialized a subject in the global example group to be subject { Run.new }. This is the basis of what drives the code which you can frequently see in Rails applications:

describe Post do
  it { is_expected.to validate_presence_of(:title) }
end

Subjects can also be referenced explicitly, via subject:

describe Run do
  subject do
    Run.new(:duration => 32,
            :distance => 5.2,
            :timestamp => "2014-12-22 20:30")
  end

  describe "#timestamp" do
    it "returns a DateTime" do
      expect(subject.timestamp).to be_a(DateTime)
    end
  end
end

However, we recommend that you do not use subject like this. There are other ways which reveal the intent of code more clearly, as we will see below.

Before Hooks

In order to write a test, we often need to bring the world to a certain state first. For example, let's say that we are describing a method that should return the total number of logged runs: Run.count. This method can also optionally receive a parameter to limit the scope to one week. Before we call it, we need to log a number of runs first.

RSpec's before hook is a convenient way to structure code which should run before every example, as in the following spec:

describe RunningWeek do

  describe ".count" do

    context "with 2 logged runs this week and 1 in next" do

      before do
        2.times do
          Run.log(:duration => rand(10),
                  :distance => rand(8),
                  :timestamp => "2015-01-12 20:30")
        end

        Run.log(:duration => rand(10),
                :distance => rand(8),
                :timestamp => "2015-01-19 20:30")
      end

      context "without arguments" do
        it "returns 3" do
          expect(Run.count).to eql(3)
        end
      end

      context "with :week set to this week" do
        it "returns 2" do
          expect(Run.count(:week => "2015-01-12")).to eql(2)
        end
      end
    end
  end
end

Note how we have not only managed to avoid duplication, but also to have nicely readable test examples which contain only the essence of the test.

When you write before, it is the equivalent of writing before(:each), which means "run this code before each example". You can also say before(:all) which would run the code only once for the given context. If you need, you can also define an after hook, with the same variants. You can read more about hooks in RSpec documentation.

Let Helper

RSpec's let helper is a way to define all dependent objects for test examples. If you need to reference the same "thing" in more than one example, and it cannot be made a subject, that is a good use case for let.

The code that is placed inside a let block is lazily evaluated: it is executed only the first time a test example calls it and is cached for further calls in the same example. If you need to force the method to be invoked every time, use let!.

Let's say we realized that our code for providing weekly running statistics would be placed in a RunningWeek class.

describe RunningWeek do

  let(:monday_run) do
    Run.new(:duration => 32,
            :distance => 5.2,
            :timestamp => "2015-01-12 20:30")
  end

  let(:wednesday_run) do
    Run.new(:duration => 32,
            :distance => 5.2,
            :timestamp => "2015-01-14 19:50")
  end

  let(:runs) { [monday_run, wednesday_run] }

  let(:running_week) { RunningWeek.new(Date.parse("2015-01-12"), runs) }

  describe "#runs" do

    it "returns all runs in the week" do
      expect(running_week.runs).to eql(runs)
    end
  end

  describe "#first_run" do

    it "returns the first run in the week" do
      expect(running_week.first_run).to eql(monday_run)
    end
  end

  describe "#average_distance" do

    it "returns the average distance of all week's runs" do
      expect(running_week.average_distance).to be_within(0.1).of(5.4)
    end
  end
end

Contrast this code with the example for the before hook, where we needed to prepare some data but did not care about it later. In this spec we need to both prepare some data and easily reference it in test examples.

Also, note the use of composed matchers in the last example (be_within(...).of(...)). RSpec's matchers can be composed, which helps improve code readability. If you are interested in seeing the full list of matchers that exist and can be composed, browse the documentation of classes and methods in RSpec::Matchers::BuiltIn.

Instance Variables in Before Hooks vs Let

At this point you may be wondering why not just use instance variables in before blocks instead of let:

describe RunningWeek do
  before do
    @monday_run = Run.new(...)
  end
end

There are actually three approaches to the extent of the use of let:

  1. Using let to define all object dependencies and keep the body of examples minimal.
  2. Use it occasionally to avoid duplication by defining "variables" when in need to reference the same thing in multiple test examples, but not much more.
  3. Not using it at all; relying on instance variables in before hooks only.

We encourage you to adopt the first approach, combined with additional data setup in a before hook when necessary. It results in very readable code, is less error-prone (typos in instance variables do not raise exceptions) and may give better performance due to lazy evaluation. RSpec maintainer Myron Marston recommends it as well.

Exception Handling

We often need to specify that a given method or block of code should raise an exception. For this purpose you can use the raise_error matcher (or its equivalent, raise_exception):

describe RunningWeek do

  describe "initialization" do

    context "given a date which is not a Monday" do

      it "raises a 'day not Monday' exception" do
        expect { RunningWeek.new(Date.parse("2015-01-13"), []) }.to raise_error("Day is not Monday")
      end
    end
  end
end

In general you can narrow the expectation to be based on the error message and/or class.

If your code raises an exception and it is not followed by this matcher, the spec will fail.

Wrapping Up

After reading this and the first RSpec tutorial, you should have a solid foundation to start using RSpec in your Ruby projects. If you are interested in a bit more advanced topics of object mocking and method stubbing, continue reading the series below. We will also publish more tutorials that show how to effectively use RSpec with Rails.

P.S. Semaphore is working on a book "The Ultimate Guide to BDD with Rails". Sign up to receive a FREE copy.

4062b608b628a89adeb54ec61e464c32
Marko Anastasov

Rendered Text / Semaphore cofounder.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.