Gentle introduction to RSpec

Writing useful tests is as important as writing the right code. The good news is that with Ruby, testing the code is a friendly and grateful task. The bad news is that it might be a little hard for you if you never wrote a single line of code. However, once you understand why tests are needed and how to write efficient tests, it will become a habit to always assure good code coverage for things you create.

RSpec is a domain-specific language (and Ruby makes implementing DSL’s easy!) created to test Ruby’s code. Simple as that. Because it’s a DSL, it allows you to easily read code that becomes a part of the documentation. Experience in RSpec is mentioned in most job offers these days, so it’s valuable knowledge and may be beneficial in the future for you if you plan to work as a Ruby developer. And yes, you can use it outside of Rails.

RSpec installation

RSpec is available as a gem, but it’s a meta-gem, which means that it depends on other gems. The following gems are the main libraries:

  • RSpec core - it includes the rspec command, which allows to execute tests and receive a meaningful output about the progress and results. It also contains the structure for writing executable examples of how your code should behave.
  • RSpec expectations - it includes the logic for expressing expected outcomes as an object in the example.
  • RSpec mocks - it includes a test-double framework for rspec with support for method stubs, fakes, and message expectations on generated test-doubles and real objects alike.

The main reason behind such structure is that you can use some elements of RSpec with other test frameworks like Test::Unit.

Gem installation

Chose the version of Ruby do you prefer and install the gem, as usual, using the gem install command:

gem install rspec

Now that we have the library installed in our system, we can generate configuration files.

Configuration creation

You can automatically generate configuration files by running the following command in your command line:

rspec --init

The above command creates two files:

  • .rspec - it’s a preferences file that should not be tracked by GIT and placed in your remote repository if you are using one. This file contains settings specific to you. For example, command parameters that should always be invoked
  • spec/spec_helper.rb - configuration file should be tracked by GIT because it contains settings specific to the project you are working on. The same configuration should be shared among all developers participating in the project. After generation, it has default settings.

You can now run the following command:

rspec

And you should see output telling you that there are no tests yet. We are ready to write our first test.

RSpec’s test structure

Before we write our first test, let’s start by writing a code that we can test. It is good to follow the Test Driven Development approach, but I won’t do this now to simplify this article’s process.

Creating the code that we can test

How about creating a Person class that will allow us to pass the name and age and receive the necessary information, such as the first name, last name, and information if the current person is an adult?

Create a new file called person.rb and put there the following code:

class Person
  def initialize(name:, age:)
    @name = name.to_s
    @age = age.to_i
  end

  def first_name
    @name.split(' ').first
  end

  def last_name
    @name.split(' ').last
  end

  def adult?
    @age >= 18
  end
end

Now when we know what we would like to test, we can finally start writing the test code.

Naming conventions

With the configuration creation, a directory called spec was created. This is the directory where we will put all test files and any files related to tests like fixtures, factories, and support files (you will learn about them later).

A common practice is to reflect the file structure when creating tests structure inside the spec directory, but since we have only one class, we can create the test without putting it into an extra namespace. The file name pattern is straightforward: get a class name and add _spec suffix to it.

In our case, we would like to create a person_spec.rb file:

touch spec/person_spec.rb

The test is empty, so RSpec won’t find it when calling the rspec command.

The test skeleton

In a test file, we are describing the expected behavior of the given class. To let RSpec know what class we are testing, we have to wrap the test definition with RSpec.describe block:

require './person'

RSpec.describe Person do

end

The class name is not mandatory; you can use string as well. When passing the class name, you have access to the described_class variable that you can refer to instead of the original class name. Such an approach is useful when you want to change the class name - you just have to update the main describe block instead of multiple places inside the test. At the top of the test, we are loading our Person class.

Describing the method

We can now begin describing the behavior of the first method: first_name. Since we are inside the RSpec’s block, we don’t have to use the RSpec prefix, we can directly call describe, and it will be executed in a proper context:

require './person'

RSpec.describe Person do
 describe '#first_name' do
   it 'returns first part of the name' do
    
   end
 end
end

When calling the rspec command, you will notice that it finds the test, and the output contains information about it. Because we are not testing anything, the test passes.

RSpec expectations

I mentioned before that in the test, we are describing the expected behavior of a given class and its methods. Tests are executed to ensure that the code is working as we expect it to work.

The simplest definition of an expectation is the following:

expect(something).to be_or_do_something

So when adding two numbers, for example, 1 and 3, we expect the result to be 4:

expect(1 + 3).to eq(4)

Try to read the above line like a standard sentence. It makes sense. In our case, we expect that by passing John Doe as a name to our Person class, the first_name method will return John:

require './person'

RSpec.describe Person do
 describe '#first_name' do
   it 'returns first part of the name' do
     person = Person.new(name: 'John Doe', age: 21)

     expect(person.first_name).to eq('John')
   end
 end
end

Run the rspec command, and you will see that our test passed. The expectation we used is the simplest one available in the library. We could also add another example to ensure that the value returned by the first_name method does not contain last_name, but it does not make sense - avoid writing a test that doesn’t test anything essential. The first example already ensures that the last name is not included.

Now when you know how to create the expectation, we can add tests for other methods also:

require './person'

RSpec.describe Person do
 describe '#first_name' do
   it 'returns first part of the name' do
     person = Person.new(name: 'John Doe', age: 21)

     expect(person.first_name).to eq('John')
   end
 end

 describe '#last_name' do
   it 'returns last part of the name' do
     person = Person.new(name: 'John Doe', age: 21)

     expect(person.last_name).to eq('Doe')
   end
 end

 describe '#adult?' do
   it 'returns true when a person is more than 17 years old' do
     person = Person.new(name: 'John Doe', age: 21)

     expect(person.adult?).to eq(true)
   end

   it 'returns false when a person is less than 18 years old' do
     person = Person.new(name: 'John Doe', age: 17)

     expect(person.adult?).to eq(false)
   end
 end
end

You can now experiment with the code and try to introduce the bug in the Person class - one of the examples will fail, and that will be clear information to you that the code is not working as you expected.

Tests as a documentation

I mentioned before that a well-written test is an excellent part of the documentation. If someone is new to the project and would like to know more about the Person class, he can run the rspec command with --format documentation flag to see a beneficial output:

rspec --format documentation
Person
  #first_name
    returns first part of the name
  #last_name
    returns last part of the name
  #adult?
    returns true when a person is more than 17 years old
    returns false when a person is less than 18 years old

Finished in 0.00181 seconds (files took 0.07689 seconds to load)
4 examples, 0 failures

With the above method, you exactly know what the given method is responsible for. Writing a good test description is a kind of art, just like naming the methods and classes.

Begin a journey to quality with RSpec

This article is just a beginning, a short introduction to the world of Ruby code tests. In the next articles, I will describe different types of tests and more advanced techniques that you can use to be sure that your code is working as you expected.

If you would like to learn more now, make sure you visit the documentation pages: https://rspec.info/ and https://relishapp.com/rspec/.

Happy testing!