15 Jul 2022 · Greatest Hits

Getting Started with RSpec

11 min read
Contents

RSpec is a testing tool for Ruby, created for behavior-driven development (BDD). It is the most frequently used testing library for Ruby in production applications. Even though it has a very rich and powerful DSL (domain-specific language), at its core it is a simple tool which you can start using rather quickly. This RSpec tutorial will help you get started, assuming you have no prior experience with the library and even testing.

The Idea Behind BDD

To understand why RSpec is the way it is, we need to understand the point of BDD and its parent, TDD.

The idea of test-driven development (TDD) was first brought to a wider audience by Kent Beck in his 2000 book Extreme Programming Explained. Instead of always writing tests for some code that we already have, we work in a red-green loop:

  • Write the smallest possible test case that matches what we need to program.
  • Run the test and watch it fail. This gets you into thinking how to write only the code that makes it pass.
  • Write some code to make the test pass.
  • Run your test suite. Repeat steps 3 and 4 until all tests pass.
  • Go back and refactor your new code, making it as simple and clear as possible while keeping the test suite green.

This workflow implies a “step zero”: taking time to think carefully about what exactly it is that we need to build, and how. When we always start with the implementation, it is easy to lose focus, write unnecessary code, and get stuck.

Behavior-driven development is an idea built on top of TDD. The idea is to write tests as specifications of system behavior. It is about a different way of approaching the same challenge, which leads us to think more clearly and write tests that are easier to understand and maintain. This in turn helps us write better implementation code.

A common problem newcomers face when starting with testing is falling into the trap of writing tests which do too much, test too little and require deep focus in order to understand what is going on.

def test_making_order
  book = Book.new(:title => "RSpec Intro", :price => 20)
  customer = Customer.new
  order = Order.new(customer, book)

  order.submit

  assert(customer.orders.last == order)
  assert(customer.ordered_books.last == book)
  assert(order.complete?)
  assert(!order.shipped?)
end

The example above is written with test-unit, a unit testing framework that’s part of Ruby’s standard library.

With RSpec, we can get a little more verbose, describing behavior for the sake of clarity:

describe Order do
  describe "#submit" do

    before do
      @book = Book.new(:title => "RSpec Intro", :price => 20)
      @customer = Customer.new
      @order = Order.new(@customer, @book)

      @order.submit
    end

   describe "customer" do
     it "puts the ordered book in customer's order history" do
       expect(@customer.orders).to include(@order)
       expect(@customer.ordered_books).to include(@book)
     end
   end

   describe "order" do
     it "is marked as complete" do
       expect(@order).to be_complete
     end

     it "is not yet shipped" do
       expect(@order).not_to be_shipped
     end
   end
  end
end

It is worth noting that for a full BDD cycle we would need a tool such as Cucumber to write an outside-in scenario in human language. This also acts as a very high level integration test, making sure the application works as expected from the user’s perspective.

Now that we’ve covered the idea behind BDD, it’s time to continue our quest to learn the basics of RSpec.

RSpec Basics

This RSpec tutorial will be based on implementing a part of a string calculator. The plan is to:

  • Create a simple string calculator with a method int Add(string numbers)
  • The method can take 0, 1 or 2 numbers, and will return their sum. For an empty string it will return 0. For example, input can be “” or “1” or “1,2”.
  • Allow theAdd method to handle an unknown amount of numbers.

Setting Up RSpec

Let’s start a new Ruby project where we’ll configure RSpec as a dependency via Bundler.

Create a new directory and put the following code in your Gemfile:

# Gemfile
source "https://rubygems.org"

gem "rspec"

Open your project’s directory in your terminal, and type bundle install --path .bundle to install the latest version of RSpec and all related dependencies. You’ll see output similar to the one below:

$ bundle install --path .bundle
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Using bundler 2.1.4
Fetching diff-lcs 1.3
Installing diff-lcs 1.3
Fetching rspec-support 3.9.2
Installing rspec-support 3.9.2
Fetching rspec-core 3.9.1
Installing rspec-core 3.9.1
Fetching rspec-expectations 3.9.0
Installing rspec-expectations 3.9.0
Fetching rspec-mocks 3.9.1
Installing rspec-mocks 3.9.1
Fetching rspec 3.9.0
Installing rspec 3.9.0
Bundle complete! 1 Gemfile dependency, 7 gems now installed.
Bundled gems are installed into `./.bundle`

Writing the First Spec

By convention, tests written with RSpec are called “specs” (short for “specifications”) and are stored in the project’s spec directory. Create that directory in your project too:

mkdir spec

Let’s begin writing our first spec. That’s right, we’re going to start by writing a spec of the string calculator, and not the string calculator itself!

# spec/string_calculator_spec.rb
describe StringCalculator do
end

With RSpec, we are always describing the behavior of classes, modules and their methods. The describe block is always used at the top to put specs in a context. It can accept either a class name, in which case the class needs to exist, or any string you’d like.

Since Ruby methods do not require the use of parenthesis, this file already begins to feel more like an essay, rather than computer code. That’s exactly the goal.

To run the specs, type:

$ bundle exec rspec

Do this now, and your spec will fail with the uninitialized constant StringCalculator error. That’s expected, as we haven’t created that class yet.

Create a new directory called lib:

mkdir lib

Declare StringCalculator in string_calculator.rb:

# lib/string_calculator.rb
class StringCalculator
end

And require it in your spec:

# spec/string_calculator_spec.rb
require "string_calculator"

describe StringCalculator do
end

Running RSpec now passes:

$ bundle exec rspec
No examples found.

Finished in 0.00068 seconds (files took 0.30099 seconds to load)
0 examples, 0 failures

What we’ve accomplished here is that we have established a working configuration of our project. We have a functional feedback loop that includes tests and application code.

So let’s proceed by writing some code.

The simplest thing our string calculator can do is accept an empty string, in which case we might decide we want it to return a zero. The method we need to describe first is add.

# spec/string_calculator_spec.rb
describe StringCalculator do

  describe ".add" do
    context "given an empty string" do
      it "returns zero" do
        expect(StringCalculator.add("")).to eq(0)
      end
    end
  end
end

There are a couple of new things here:

  • We are using another describe block to describe the add class method. By convention, class methods are prefixed with a dot (".add"), and instance methods with a dash ("#add").
  • We are using a context block to describe the context under which the add method is expected to return zero. context is technically the same as describe, but is used in different places, to aid reading of the code.
  • We are using an it block to describe a specific example, which is RSpec’s way to say “test case”. Generally, every example should be descriptive, and together with the context should form an understandable sentence. This one reads as “add class method: given an empty string, it returns zero“.
  • expect(...).to and the negative variant expect(...).not_to are used to define expected outcomes. The Ruby expression they are given (in our case, StringCalculator.add("")) is combined with a matcher to fully define an expectation on a piece of code. The matcher we are using here is eq, a basic equality matcher. RSpec comes with many more built-in matchers.

If we run our spec now, we will get a failure that the method is not defined:

$ bundle exec rspec
F

Failures:

  1) StringCalculator.add given an empty string returns zero
     Failure/Error: expect(StringCalculator.add("")).to eq(0)

     NoMethodError:
       undefined method `add' for StringCalculator:Class
     # ./spec/string_calculator_spec.rb:8:in `block (4 levels) in <top (required)>'

Let’s write the minimum amount of code to make that spec pass:

# lib/string_calculator.rb
class StringCalculator

  def self.add(input)
    0
  end
end

We want to write the simplest possible code which will make the specs pass, remember? If you run bundle exec rspec now, the spec does pass.

Towards Working Code

Our next assignment is to make the calculator work given a single number in a string. Let’s write some RSpec examples for that:

# spec/string_calculator_spec.rb
describe StringCalculator do

  describe ".add" do
    context "given '4'" do
      it "returns 4" do
        expect(StringCalculator.add("4")).to eql(4)
      end
    end

    context "given '10'" do
      it "returns 10" do
        expect(StringCalculator.add("10")).to eql(10)
      end
    end
  end
end

After we have run the specs, we will get some helpful output:

$ bundle exec rspec
.FF

Failures:

  1) StringCalculator.add given '4' returns 4
     Failure/Error: expect(StringCalculator.add("4")).to eq(4)

       expected: 4
            got: 0

       (compared using ==)
     # ./spec/string_calculator_spec.rb:14:in `block (4 levels) in <top (required)>'

  2) StringCalculator.add given '10' returns 10
     Failure/Error: expect(StringCalculator.add("10")).to eq(10)

       expected: 10
            got: 0

       (compared using ==)
     # ./spec/string_calculator_spec.rb:20:in `block (4 levels) in <top (required)>'

Finished in 0.01715 seconds (files took 0.08149 seconds to load)
3 examples, 2 failures

Again, our goal is to make them pass:

# lib/string_calculator.rb
class StringCalculator

  def self.add(input)
    if input.empty?
      0
    else
      input.to_i
    end
  end
end

Time to make the calculator actually do some math. Let’s write some examples based on strings containing comma-separated numbers. It might make sense to introduce a nested context, “two numbers”:

# spec/string_calculator_spec.rb
describe StringCalculator do

  describe ".add" do
    context "two numbers" do
      context "given '2,4'" do
        it "returns 6" do
          expect(StringCalculator.add("2,4")).to eql(6)
        end
      end

      context "given '17,100'" do
        it "returns 117" do
          expect(StringCalculator.add("17,100")).to eql(117)
        end
      end
    end
  end
end

These specs fail, as you’d expect. The full output is omitted here for brevity, but I encourage you to run your specs after every change in code.

Here’s one way to make the specs pass:

class StringCalculator

  def self.add(input)
    if input.empty?
      0
    else
      numbers = input.split(",").map { |num| num.to_i }
      numbers.inject(0) { |sum, number| sum + number }
    end
  end
end

RSpec has more than one way to display its output. A very popular alternative to the default dot format is the “documentation” format:

$ bundle exec rspec --format documentation

StringCalculator
  .add
    given an empty string
      returns zero
    single numbers
      given '4'
        returns 4
      given '10'
        returns 10
    two numbers
      given '2,4'
        returns 6
      given '17,100'
        returns 117

In this tutorial, we covered the basic building blocks of RSpec. When you browse RSpec’s built-in matchers (see references below), you will be ready to start writing your first tests.

There is definitely more to RSpec though, and that is a topic of the next tutorial in this series.

The full source code is available in a GitHub repository as a working project.

Continuous Integration for RSpec and Ruby on Semaphore

After you’ve written your first RSpec tests, the next step is to automate running them on every git push by setting up continuous integration (CI).

Semaphore is a hosted CI service which comes with comprehensive Ruby support, and it’s very easy to get started. It also comes with a neat Test Reports feature that allows you to see which tests have failed, find skipped tests and the slowest tests in your test suite.

This is the Semaphore configuration that you can use for continuous integration:

# .semaphore/semaphore.yml
version: v1.0
name: Ruby
agent:
  machine:
    type: e1-standard-2
    os_image: ubuntu1804
blocks:
  - name: RSpec
    task:
      jobs:
        - name: Run specs
          commands:
            - checkout
            - sem-version ruby 2.6.0
            - bundle install --path vendor/bundle
            - bundle exec rspec

RSpec references from tutorial

Here are some links to documentation for the things mentioned in this tutorial. It is always a good idea to study the docs to learn what else exists in the API or DSL that you are using.

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