Gentle introduction to RSpecLearn how to write useful tests for Ruby code
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 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.
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.
You can automatically generate configuration files by running the following command in your command line:
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:
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.
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:
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.
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:
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.