Write Modular RSpec

RSpec is the most popular test framework in Ruby/Rails environments. Two of its biggest benefits is the ability to write clean, concise, and modular tests and combine it with many different frameworks (backend, frontend, API..etc.) One of the biggest pains in writing test scripts is their refactoring which is inevitable. This article aims to demonstrate how to make test scripts that are easy to understand and maintain, along with reducing refactoring nightmares.

Your Imaginary Project

For the sake of this article, we will start with following premise:
1. Your site (“Weather is Good”) is an application that offers information about weather forecasts and current weather in some areas based on user input.
2. It exposes a REST API (data format is JSON).
3. In order to access the API, users need to register and obtain an authorization token used for subsequent requests.

Methods that are offered through API are:

GET/
– Current weather in a city (token, city)
– Weather in an area of city (token, city, area)
– User input on weather in city (token, city)

The Problem

The project has just started. You heard about this RSpec tool and you want to use it as main framework for writing tests. You have never worked with it and are new to Ruby. You are slowly progressing with writing your tests and are learning more RSpec/Ruby every day. You currently have some methodology for writing your tests and it’s working. Good! As the project progresses and the application has more and more features, you are comfortable with writing your test scripts. It becomes second nature to you, but you are starting to realize that you’re writing the same code in multiple spots. You don’t have time to refactor existing test scripts and you fear of losing time and breaking things that are already working.

It is often said: “If it ain’t broken, don’t fix it.” But in this case, that does not apply. There are many test scripts and, eventually, we realize that they are not maintainable. Technical dept is piling up because we didn’t write the specs in a smart way.

The Solution

Test refactoring cannot be avoided, simply because features change daily. The question is: Will you do it the easy or hard way? To do it the easy way, you need to write tests modularly to avoid code duplication. Considering the API that is exposed in the application, here are some possible use cases to cover:

Create a new user and verify it is possible to login and logout with this user.

NEG – Try to login with non-existing user.

With an existing user, get weather forecast for a particular city (city exists).

NEG – With an existing user, get weather forecast for a particular city (city does not exist).

With an existing user, get weather forecast for a particular area of the city.

NEG – With an existing user, get weather forecast for p articular area of the city (area is in minus range).

With an existing user, get input from users for a particular city.

With an existing user, add new input for particular city and check this input is in the list of inputs.

NEG – Delete existing user and try to login with this user.

Delete existing user, create new user, login with this user and ensure that comments from previous user for particular city have been deleted.

These are just some of the cases and, certainly, there are more cases to be covered. Sticking to the cases described here, there are some reusable patterns already. We will need a common class to provide methods for sending REST requests. Also, some actions will be repeatable, like creating a new user, login, logout, getting the forecast for a city, getting user input for weather in a city, etc.

Setting up RSpec

First things first, it’s time to setup RSpec. Use the standard rspec --init command which sets up the initial structure for the RSpec tests:

<root_project_directory>
- spec
spec_helper.rb
.rspec

Since the tests will be modular and will use some initial config data, a good way to start is to create two additional directories: config and lib under the project root directory. The config for each environment will live in a config.yml. The lib will hold reusable code for our test scripts.

--ADVERTISEMENT--

Setting Up config.yml

The config.yml looks like:

environment:
server: http://test.weatherisgood.com
port: 7000

Reusable Parts

As previously stated, we need a common class to send REST requests. This will be the Sender class inside sender.rb file:

The code is pretty self-explanatory. In the constructor, create server_url from the configuration data. The send_request method specifies the endpoint, REST method, possible arguments, and headers (for authentication). Now, we have centralized class used for sending and parsing REST requests.

The next thing we need is an implementation of common actions (login/logout, create new user, get weather forecast..etc.) For the sake of simplicity, these methods live inside a single *.rb file, but we could use multiple files to divide code base on functionality. Let’s call it weatherisgood_api.rb (Paradoxically, naming is one of the hardest things in software development and there are likely better names for this :) ):

As you can see, there are methods for each of the actions that will be performed against the API. Logging in gets the authorization token to be used in subsequent requests.

RSpec Reusable Magic

With the base implementation for REST calls and common actions, we can utilize the RSpec sharing concept to further divide actions performed in the test steps. This is important because certain test steps will provide different input for these actions and expect different output (A good example is positive and negative tests for login.) For this purpose, use RSpec’s shared_context which defines actions that take input parameters and perform checks inside of it blocks in the same manner as regular describe blocks:

Notice that I use the prefix “NEG” in some shared contexts to indicate a negative check (for example, login with invalid credentials). shared_contexts are called from test cases. Create testcase1_spec.rb and place it inside the spec directory. I usually put one test case in one *.rb file for the sake of understanding and maintenance. Here’s an example:

require 'rubygems'
require './lib/weatherisgood_api'
require './lib/shared/weatherisgood_spec'
user = "bakir"
password = "mypassword"
city = "Sarajevo"
city_doesnot_exist = "MyCity"
weatherisgood = WeatherIsGoodApi.new
describe "Test Case 1: Login with user and get weather check for city that exists and doesn't exist" do
context "Login with user #{user}" do
include_context "Login", weatherisgood, user, password
end
context "Get weather for city #{city} that exists" do
include_context "Get weather for city", weatherisgood, city
end
context "Get weather for city #{city_doesnot_exist} that does not exist" do
include_context "NEG - Get weather for city", weatherisgood, city_doesnot_exist
end
end

A few things to notice:
1. I’m using context to divide the test steps.
2. The test case is described using describe. Test steps are described using context. This is a personal preference to differentiate between test case and test step.
3. Inside every context we have include_context and the name of the shared_context to invoke, passing parameters as necessary.
4. When running this test case, the output (using --format documentation in .rspec file) looks like:

Test Case 1: Login with user and get weather check for city that exists and doesn't exist
Login with user bakir@myemail.com
succeeds for user: bakir@myemail.com
Get weather for city that exists
gets weather forecast for city Sarajevo
Get weather for city that does not exist
does not get weather forecast for city MyCity that does not exist
Finished in 0.00124 seconds
3 examples, 0 failures

As you can see, the output is very clear and includes:
– Description of the test step (“Login with user bakir@myemail.com”)
– Expected result of the test step (“succeeds for user: bakir@myemail.com”)
It’s advisable to describe your test steps as clearly as possible so the output is readable and useful.

Conclusion

In this tutorial I’ve tried to explain why we need reusability in our RSpec test code. Every change needed for the REST api can be made in one place (the WeatherIsGoodApi class). Also, expected results for the particular feature under test is adjusted in a single location place (shared_context in weatherisgood_spec.rb) This is one approach for reusability, but you could certainly come up with others. The Point is: Think before you write tests, because it can easily bite you in the long run. If that happens, the process of coming back and changing the design of your tests can be worse than writing them from scratch.

IT professional with 4+ years experience in software automation and testing who is currently employed in AtlantBH. Always willing to learn new technologies and improve my skills as well as improve existing software solutions on which I'm working