RSpec: Basic concepts

Testing with RSpec: Part I – Basic concepts

Introduction

Hello, fellow reader! Let’s explore the fundamental concepts for the Ruby gem RSpec together. We’ll cover its basic structure, progressing from a comprehensive to a complete test case. By the end, the basic knowledge of RSpec features will have been explained, and how to use them. Now let’s dive into RSpec features.

RSpec

RSpec is a ruby gem, composed of multiple gems that work together:

For this first post, we’re not gonna cover rspec-mocks and rspec-rails. This will be a part II or III.

describe

describe is a method that accepts in most cases a string or a class as argument. Often used to describe an object and/or an action. So we can use the describe method to organize our test file in multiple blocks, normally one for each message that an object can receive.

RSpec.describe Greeter do
  describe '#hello' do
  end
end

In this simple example, we are describing how the class Greeter and the instance method #hello work. Let’s keep going, we have more to cover.

context

Similar to describe, context is a method provided by RSpec and, as its name suggests, is what we can use to give specific scenarios (contexts) for a use case. By using context we can keep our tests clean and organized.

With context, it is possible to give a description of the different behaviors that a method might have. When writing the description, a good tip is to use the words "when", "with" or "without" at the beginning (see betterspecs). Let’s continue our previous example.

RSpec.describe Greeter do
  describe '#hello' do
    context 'when a name is given' do
    end

    context 'when the name is nil' do
    end
  end
end

Great! Now we have at least two different behaviors for Greeter#hello. But what happens in each context? This is what we’re going to find out next.

it

it is the last part of a test structure, here is where it can be written the actual result for each context.

RSpec.describe Greeter do
  describe '#hello' do
    context 'when a name is given' do
      it 'returns a greeting message with the given name' do
      end
    end

    context 'when the name is nil' do
      it 'returns a greeting message without a name' do
      end
    end
  end
end

At this point, we can imagine that Greeter#hello returns a message that might have a name, if provided.

  • TIP: Note that we build our test structure by nesting describe, context, and it, which is good but can become hard to read if we nest too many contexts. There is no specific number of contexts but it is good to use a limit to keep the codebase organized and use this limit as a warning. This way, if the nesting is increasing, it might be an indicator that the object/method under test is complex and would be good thing to refactor it.

Ok, we have the structure, but now what? How do we test the object? This is where some useful RSpec methods take place. First, we’re gonna cover subject.

subject

As mentioned before, subject is another method that can be used to describe the object under test in multiple examples. Here’s how it can be used.

RSpec.describe Greeter do
  subject(:greeter) { described_class.new(name) }

  describe '#hello' do
    context 'when a name is given' do
      it 'returns a greeting message with the given name' do
      end
    end

    context 'when the name is nil' do
      it 'returns a greeting message without a name' do
      end
    end
  end
end

— OK, what happened? Why is it called :greeter? What is described_class? name? What?

No worries, let’s cover it step by step. First, :greeter is a symbol that can be used to give a name to the subject, this is not required but might help the test readability. Second, described_class returns the class provided on the first describe. In this example Greeter is the same as subject(:greeter) { Greeter.new(name) }. And what about name? This is where the let method takes place. Let’s take a look.

let

Same as before, let is a method that will define another one. We can think of it as creating a variable that can be referenced throughout the test. When using let we have a lazy loading, in which the method defined by it will only be created when it is called for the first time. There is a variant for let which is let!, and the main difference is that let! is defined while defining a block. It is usually used to seed the test database previously before the test runs, so we don’t need to call it to create/instantiate the object defined. Let’s see how we can use let for Greeter#hello.

RSpec.describe Greeter do
  subject(:greeter) { described_class.new(name) }

  describe '#hello' do
    context 'when a name is given' do
      let(:name) { 'Tony' }

      it 'returns a greeting message with the given name' do
      end
    end

    context 'when the name is nil' do
      let(:name) { nil }

      it 'returns a greeting message without a name' do
      end
    end
  end
end

Now that we have defined name, the greeter subject will know which value to use in each context. While defining a method with let, it can be possible to easily lose track of definitions for different contexts which can lead to misinterpretation of the test. So a good tip is to avoid when possible global let like a subject.

Ok, this is good and all but where is the expectation? Let’s wrap it up now.

expect

As the name suggests, it is a method used to do the test assertions for an it block. The expect method can be used to assert certain behaviors, including errors. Now that we have everything done, let’s complete our test.

RSpec.describe Greeter do
  subject(:greeter) { described_class.new(name) }

  describe '#hello' do
    context 'when a name is given' do
      let(:name) { 'Tony' }

      it 'returns a greeting message with the given name' do
        expect(greeter.hello).to eq('Hi, my name is Tony.')
      end
    end

    context 'when the name is nil' do
      let(:name) { nil }

      it 'returns a greeting message without a name' do
        expect(greeter.hello).to eq('Hi, my name is .')
      end
    end
  end
end

We covered the basics of RSpec testing, from its fundamental structure to a complete test case. We are going to dive into more details in the next posts, especially Ruby on Rails tests for models and request specs. So stay tuned: there will be more to cover in the next posts. See you soon.

We want to work with you. Check out our "What We Do" section!