13 Mar 2020 · Software Engineering

    RSpec Subject, Helpers, Hooks and Exception Handling

    8 min read
    Contents

    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 in Mocking With RSpec: Doubles and Expectations.

    P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Marko Anastasov is a software engineer, author, and co-founder of Semaphore. He worked on building and scaling Semaphore from an idea to a cloud-based platform used by some of the world’s engineering teams.