ruby

Behaviour Driven Development in Ruby with RSpec

Thomas Riboulet

Thomas Riboulet on

Behaviour Driven Development in Ruby with RSpec

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.

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!

Thomas Riboulet

Thomas Riboulet

Our guest author Thomas is a Consultant Backend and Cloud Infrastructure Engineer based in France. For over 13 years, he has worked with startups and companies to scale their teams, products, and infrastructure. He has also been published several times in France's GNU/Linux magazine and on his blog.

All articles by Thomas Riboulet

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps