Behaviour Driven Development in Ruby with RSpec
Tags: ruby, rspec
Published: 2024-02-07 11:32:58

RSpec is a library for writing and running tests in Ruby applications. As its landing page states, RSpec is: "Behaviour Driven Development for Ruby. Making TDD productive and fun". We will return to that last part later.

This post, the first of a two-part series, will focus on introducing RSpec and exploring how RSpec especially helps with Behaviour Driven Development in Ruby.

Let's dive in!

## The History of RSpec

RSpec started in 2005 and, through several iterations, reached version 3.0 in 2014. Version 3.12 has been available for almost a year. Its original author was Steven Baker, but the mantle has passed to several maintainers through the years: David Chelimsky, Myron Marston, Jon Rowe, and Penelope Phippen. Many contributors have also been part of the project.

When you read RSpec's good practices, you see this phrase early on: "focus on testing the behavior, not the implementation". That's not a strategy specific to RSpec; it's good advice for anyone writing tests.
In more practical terms, you should focus your tests on how the code you are testing behaves, not how it works.

For example, to test that a user's email address is valid, you should test that the `validate_email` method returns `false` when the email address is invalid. You are not testing a specific implementation but rather how that code reacts (i.e., behaves) when handling different strings that should be email addresses.

Behavior interests us: how the code acts and defines how our application will work. Furthermore, this approach simply lets us know whether things work (or not), and we can then be more relaxed to either fix or refactor the implementation. Our tests will tell us if the code's behavior has changed; we will know if our changes have been successful or not in the most direct way possible.

The inner workings of the code don't interest us so much. Of course, we don't want a bad implementation, but measuring a good implementation is much harder than knowing if the code does what it's expected to do or not.

Let's look at how to install RSpec next.

### Installing RSpec for Ruby

RSpec comes as a gem, `rspec-core`, so you can add it to a test group in your Gemfile. It's probably best you also add `rspec`. It's a meta gem that includes `rspec-core`, `rspec-expectations`, and `rspec-mocks`. You can also add the `rspec-rails` one in a Ruby on Rails project.

Tests usually live in the `spec/` folder at the root of your project, and you can launch them by providing a path to a file or a directory: `rspec spec/models/*rb`, for example.

Now let's turn to how the RSpec DSL is set up to help with behavior testing.

## RSpec DSL: How it Helps with Testing Behavior

RSpec's whole Domain Specific Language (DSL) is completely worked around behavior testing, giving you a direct way to describe the behavior you expect from your code within different contexts. A few parts could be smoother, but overall, tests in RSpec read directly as English, much like a good piece of Ruby code.

Tests in RSpec are not written as classes, with methods taking center stage as tests. Instead, tests are written as Ruby blocks (ever used `do .. end`?), which, thanks to the method name we pass to the block as an argument, makes things very easy to read.

The primary method used in RSpec tests is `describe`. `describe` will contain one or more tests and can even contain more `describe` calls. The second method is `it`. The `it` blocks are called examples and contain the actual assumptions; they are where the testing happens. Finally, RSpec relies on "expectations" within the `it` blocks. Using the `expect` method, we define how the subject of our test is expected to behave.

Now we can start writing some simple tests.

## Simplest Tests in RSpec for Ruby: `describe` and `it`

Let's imagine a `User` class. We want a `name` method that will output first and last names together if both are present (or just one, if one is missing). Those are the different behaviors we want to test.

Here is how the simplest of those contexts look expressed as an RSpec test.

```ruby
# first call to describe, as topmost one, its description or title is used to tell what we are testing, here the User class.
# this title can be a string or a class name
describe User do

  # we are adding a second describe to regroup the tests focused on the `name` method
  describe '#name' do

    # our first example ! Note the description focusing on the behavior
    it 'returns the complete name' do

      # we define a user variable by instantiating a user
      user = User.new(first_name: 'John', last_name: 'Doe')

      # and here comes the expectation
      expect(user.name).to eq('John Doe')
    end
  end
end
```

Look at the general structure: we start by describing the focus of our test in the `User` class, the method we are testing (`name`), and then present an expected behavior.

Note how the expectation is written: we **expect** the value returned by `user.name` **to equal** `'John Doe'`. The `eq` method is a matcher. It allows us to match the tested value (on the left) and the expected one (on the right). The `expect` part is always followed with `to` or `not_to` to dictate how the matcher that follows will be used.

## Handling Multiple Contexts

While this first test shows us how it's done, it needs to catch up to what we want. It only handles one case if both first and last names are present. Let's see how we can test another case if the first name is absent.

```ruby
describe User do

  describe '#name' do
    it 'returns the complete name when both first and last name are present' do
      user = User.new(first_name: 'John', last_name: 'Doe')
      expect(user.name).to eq('John Doe')
    end

    it 'returns only the last name when the first name is missing' do
      user = User.new(last_name: 'Doe')
      expect(user.name).to eq('Doe')
    end
  end
end
```

This looks more realistic, but we still need to test another case. The description is also relatively verbose and repetitive. We could add another layer of description between the `describe '.name'` call and the example for each one. Thankfully, though, RSpec gives us a more obvious synonym for `describe` to express what we need to express for different contexts: `context`.

```ruby
describe User do

  describe '#name' do
    context 'when both first and last name are present' do
      it 'returns the complete name' do
        user = User.new(first_name: 'John', last_name: 'Doe')
        expect(user.name).to eq('John Doe')
      end
    end

    context 'when the first name is missing' do
      it 'returns only the last name' do
        user = User.new(last_name: 'Doe')
        expect(user.name).to eq('Doe')
      end
    end
  end
end
```

Thanks to this, the whole file reads even more quickly and gives us, without much thinking, an understanding of exactly which behavior we are testing within different contexts.

## Defining the Subject of Tests

We can use the `subject` method to make the subject of our tests obvious.

```ruby
describe User do

  describe '#name' do
    subject { user.name }
    context 'when both first and last name are present' do
      let(:user) { User.new(first_name: 'John', last_name: 'Doe') }

      it 'returns the complete name' do
        expect(subject).to eq('John Doe')
      end
    end

    context 'when only the first name is present' do
      let(:user) { User.new(first_name: 'John') }

      it 'returns the complete name' do
        expect(subject).to eq('John')
      end
    end
    # ... other contexts
  end
end
```

This is especially handy to avoid repetition and add clarity.

## Handling Complex Setup (Before, After Hooks) in Ruby on Rails

In many cases, we need a bit more to prepare a context. Let's take, as an example, a class method on a Ruby on Rails model named `latest_three`. It's expected to return the last three users created in the database. If we have less than that, we should get whatever users we have. By omitting the topmost `describe`, here is how a test might look.

```ruby
# note that we are using the '::method_name' here to refer to a class method, '#method_name' is reserved to refer to an instance method's name
describe '::latest_three' do
  context 'when more than three users are present' do
    it 'returns three users' do
      3.times { User.create(first_name: Faker::Name.first_name, last_name: Faker::Name.last_name) }

      expect(User.latest_three.size).to eq(3)
    end
  end

  context 'when no users are present' to
    it 'returns an empty collection' do
      expect(User.latest_three.empty?).to be(true)
    end
  end
end
```

> If you are unfamiliar with `Faker`, it's a Ruby library used to generate fake data such as names and dates through handy methods.

These two tests look ok, but the creation of the data doesn't belong to the example. It's important for the specific context, though: we need that data created **before** the example is run. To do so, we can use a `before` block. Those blocks are run before the tests that follow them (in each block's context), thus giving us a perfect opportunity to set up our data.

```ruby
describe '::latest_three' do
  context 'when more than three users are present' do
    before do
      4.times { User.create(first_name: Faker::Name.first_name, last_name: Faker::Name.last_name) }
    end

    it 'returns three users' do
      expect(User.latest_three.size).to eq(3)
    end
  end

  context 'when no users are present' to
    it 'returns an empty collection' do
      expect(User.latest_three.empty?).to be(true)
    end
  end
end
```

Once again, I hope this shows how well thought-through the RSpec DSL is. Doesn't it read nicely and give us a good understanding of each context and the behavior we expect?

If we were tempted to destroy data or execute some other form of cleanup **after** a context, we could do so through an `after` block. This is especially useful if you are writing tests using a database without the comfort of built-in automatic database cleanup between test runs.

## Avoiding Repetition with `let` in RSpec for Ruby

We still lack a few more concepts to be able to write real-world tests. Let's take the case of email validation again.

```ruby
describe '#valid_email?' do
  context 'when email does not contain an @' do
    subject(:user) { User.new(email: 'bob') }

    it { expect(user.valid_email?).to be(false) }
  end

  context 'when email does not have a tld' do
    subject(:user) { User.new(email: 'bob@appsignal') }

    it { expect(user.valid_email?).to be(false) }
  end

  context 'when email is valid' do
    subject(:user) { User.new(email: 'bob@example.org') }

    it { expect(user.valid_email?).to be(true) }
  end
end
```

There are several repetitions, to the point of blurring the actual tests. We notice that the only thing that changes, in the setup of each context, is the actual value of the email address. Couldn't we use a variable to do this?

We can use `let` blocks. They allow us to define a memoized helper method. The value is cached across multiple calls in the relative context. `let`'s syntax is similar to the one we saw for the `subject` block: first, we pass a name for the helper, then a block to be evaluated. That block is lazy-evaluated. If we don't call it, it will never be evaluated.

```ruby
describe '#valid_email?' do
  subject(:user) { User.new(email: email) }

  context 'when email does not contain an @' do
    let(:email) { 'bob' }

    it { expect(user.valid_email?).to be(false) }
  end

  context 'when email does not have a tld' do
    let(:email) { 'bob@appsignal' }

    it { expect(user.valid_email?).to be(false) }
  end

  context 'when email is valid' do
    let(:email) { 'bob@example.org' }

    it { expect(user.valid_email?).to be(true) }
  end
end
```

Note that the `subject` is moved up in the structure too: it will be evaluated within each context and thus use each context's `email` value. Here we can see the purpose of `subject` within a `describe` with multiple contexts: we define the subject of the test early to make it obvious. We can then focus on expressing each different context we want to check the subject behavior in.

### A Note on `let`

`let` is lazily defined. In the above example, `email` won't be instantiated and set until it's called. Once it is invoked, though, it's set. In effect, it's just like a memoized helper method.

Yet, in some cases, you might want to set the value associated with a `let` before the examples run. To do so, you can use `let!`. With `let!`, the defined memoized helper method is called within an implicit `before` hook for each example. In other words, the value associated with the `let!` is eagerly defined before the example is run.

Let's create a user in our context before we run our example:

```ruby
describe "#count_users" do
  let(:account) { Account.create(name: 'Acc Ltd') }
  let!(:user) { User.create(name: 'Jane', account: account) }

  it "counts the users in the account" do
    expect(account.count_users).to eq(1)
  end
end
```

This prevents us from additional setup or even a call to `user` within the before hook to get the value memoized.

### `let` Vs Instance Variables in RSpec for Ruby

Some developers might be tempted to rely on instance variables through a `describe` or `context` and their `before` hooks:

```ruby
describe "#count_users" do
  before do
    @account = Account.create(name: 'Acc Ltd')
    @user = User.create(name: 'Jane', account: @account)
  end

  it "counts the users in the account" do
    expect(@account.count_users).to eq(1)
  end
end
```

This is not very practical. It adds dependencies and state sharing between contexts, weakens isolation, and is more difficult to debug.

The additional issue is that, if you were to make a call to an instance variable that has not been initialized, you'd get a `nil` value in return. That's in contrast to the exception you'd get if you were to call a local variable that doesn't exist (raising a `NameError` exception).

So, when writing tests with RSpec, `let` is preferred, and `let!` is to be used when you need an eager evaluation. Other methods to handle variables are not recommended.

## Matchers in RSpec for Ruby

If `describe`, `context`, and `it` are very important to the structure of RSpec tests, the key part to making actual tests is matchers.

We have only seen a few, mainly `be()` and `eq()`. Those two are the simplest ones and are very handy. Here is a list of the others you should know about as a start:

- `eq`: test the equality of two objects (actually, their equivalence, the same as `==`); `expect(1).to eq(1.0) # is true`
- `eql`: test the equality of two objects (if they are identical, not just equivalent); `expect(1).to eql(1.0) # is false`
- `be`: test for object identity; `be(true)`, `be(false)` ...
- `be_nil`: test if an object is `nil`
- `be <= X`: test if a number is less or equal to a value (X); also works with `<`, `>`, `>=`, `==`
- `be_instance_of`: test if an object is an instance of a specific class; `expect(user.name).to be_instance_of(String)`
- `include`: test if an object is part of a collection; `expect(['a', 'b']).to include('a')`
- `be_empty`: test if a collection is empty; `expect([]).to be_empty`
- `start_with`, `end_with`: test if a string or array starts (or ends) with the expected elements; `expect('Brian is in the kitchen').to start_with('Brian')`, `expect([1, 2]).not_to start_with('0')`
- `match`: test if a string matches a regular expression; `expect(user.name).to match(/[a-zA-Z0-9]*/)`
- `respond_to`: test if an object responds to a particular method; `expect(user).to respond_to(:name)`
- `have_attributes`: test if an object has a specific attribute; `expect(user).to have_attributes(age: 42)`
- `have_key`: test if a key is present within a hash; `expect({ a: 1, b: 2 }).to have_key(:a)`
- `raise_error`: test if a block of code raises an error; `expect { user.name }.to raise_error(ArgumentError)`
- `change`: test that a block of code changes the value of an object or one of its attributes; `expect { User.create }.to change(User.count).by(1)`, `expect { user.activate! }.to change(user, :is_active).from(false).to(true)`
- `to_all`: test that all items in a collection match a given matcher; `expect([nil, nil, nil]).to all(be_nil)`
- `match_array`: test that one array has the same items as the expected one (the order isn't of importance); `expect([1, 3, 2]).to match_array([2, 1, 3])`

You can already write most of the tests you'll ever need with those matchers. To read more about matchers, you can check out [RSpec's documentation](https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/).

## A Few Thoughts

As you have seen, we have yet to write a line of actual code; we just wrote tests. That might be the most crucial point of this article: RSpec's DSL and structure allow you to write your test first from the behavior point of view. When you start to work on a new class, you can first express the behavior as an RSpec example within a given context. Then, simply rely on the guard rails to make your implementation a reality.

That's actually how TDD works. We are not writing tests just for the sake of tests. Instead, we write tests to express the behavior we want to see from the code. In effect, those tests are merely a transcription (through RSpec DSL) of the behavior expected for a feature.

## Wrapping Up

To summarize what we have covered in this article:

- RSpec is a library that gives us a powerful DSL to express and test the behavior of code
- `describe` is the main element to structure tests in each file
- `context` is equivalent to `describe`, but is used to separate different contexts for testing code behavior
- `it` allows us to define examples: the blocks within which tests happen
- **expectations** define the actual tests with **matchers**
- `let` and `let!` allow us to define memoized helpers through custom-named blocks to avoid repetitions; `let!` is eagerly loaded
- `subject` allows us to clearly define what is being tested and can be named

In the next post, we will look at specific types of tests for different parts of a Ruby on Rails application.

Until then, happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**