Testing your Rails code with Rspec

Mateo mojica
8 min readSep 8, 2023

--

Photo by Nguyen Dang Hoang Nhu on Unsplash

There are two sides to software development, one side is the actual development of the functionality through code and the other is testing said functionality. Both sides are equally important and many would argue that the testing part is the most important, but something that everybody will agree is that testing is the most forgotten or overlooked side of software development.

Testing your code not only will help you ensure the functionality that you created is working as you intended but also in large and mature codebases it helps to catch bugs as you change old code or expand an existing functionality down the line. Tests also can work as a way of documenting your code in the sense that they store how the code should behave and you can learn a lot by reading them, especially on large codebases with little actual documentation.

Different philosophies of coding rely heavily on tests and others that flip the perspective and have the programmer thinking about how to test the code before writing any line of code (TDD), also there are different kinds of tests that you can do each one with their caveats.

For this article we are going to focus on unit testing, meaning that we are going to be writing tests for single isolated units of code, so let’s get started.

RSpec is a testing framework that is composed of multiple libraries, which are designed to work together or can be used independently with other testing tools like Cucumber or Minitest. It has four main parts:

  • rspec-core: The spec runner, providing a rich command line program, flexible and customizable reporting, and an API to organize your code examples.
  • rspec-expectations: Provides a readable API to express expected outcomes of a code example.
  • rspec-mocks: Test double framework, providing multiple types of fake objects to allow you to tightly control the environment in which your specs run.
  • rspec-rails: Supports using RSpec to test Ruby on Rails applications in place of Rails’ built-in test framework.

To setup Rspec first, you have to add the Rspec gem to your gemfile, then run bundle install and run the command rspec –init this will create the spec folder and the basic structure to run your tests like the helper files and config files, also after you initialize Rspec it will create automatically the spec files for each component that you create(model, controller or service).

Now let’s dive into some helper methods that Rspec has by default, before(:each) and before(:all). These are hooks that allow you to execute setup code before running tests in your test suite. These are used to create the necessary state or context before your tests, such as creating objects, initializing variables, or preparing the environment for the examples.

  • before(:each): This hook is executed before each test within a test group, ensuring a clean state for each individual test.
RSpec.describe MyClass do
before(:each) do
# Setup code here will run before each example
end

it "example 1" do
# Test code
end

it "example 2" do
# Test code
end
end
  • before(:all): This hook runs once before all the examples in a test group. This can create a shared state between tests and requires additional care for test isolation.
RSpec.describe MyClass do
before(:all) do
# Setup code here will run before all examples
end

it "example 1" do
# Test code
end

it "example 2" do
# Test code
end
end

On the other hand, we have the after hooks that are executed after tests and are generally used for cleaning up or resetting the state.

  • after(:each): this hook is executed after every test in a group, ensuring cleanup tasks are performed consistently in between tests.
  • after(:all): This hook is executed once all the tests have finished, allowing to share cleanup tasks between multiple examples.

Overall, both after(:each) and after(:all) hooks are helpful for performing cleanup tasks in your test suite. They help maintain the integrity of your test environment and ensure that resources are properly released. It’s important to use them appropriately to keep your tests isolated and independent from each other.

Photo by Aaron Burden on Unsplash

When you are writing tests sometimes you need to create global objects that are accessible to all the tests, there is a really useful gem that allows you to create objects with predetermined values and attributes easily, called the fabrication gem. Fabrication generates objects in Ruby. Fabricators are schematics for your objects and can be created as needed anywhere in your app or specs. To install it just add it to your gem file in the test section and run bundle install to add it to your project. After it is installed you can start creating the fabricators for the objects or model that you want.

# located in spec/fabricators/person_fabricator.rb

Fabricator(:person) do
neighborhood
houses(count: 2)
name { Faker::Name.name }
age 45
gender { %w(M F).sample }
end

With the knowledge we have so far about the setup hooks and can create objects using the fabrication gem, we can start defining variables for our tests. let and let_it_be are two helper methods that provide a convenient way to define memoized variables within your test examples (specs). These variables can be used to set up test data or shared context and are lazily evaluated, meaning they are only calculated once and cached for subsequent use within the same example.

  • Let: This method is used to define a memoized variable within a test example. It allows you to define a value that is calculated on-demand when accessed for the first time and then cached for subsequent uses within the same example. Use let when you want to defer calculation until the variable is accessed and reuse the cached value within the same example.
RSpec.describe MyClass do
let(:my_variable) { some_expensive_calculation }

it "example 1" do
# The value of my_variable is calculated when first accessed
expect(my_variable).to eq(some_expensive_calculation_result)
end

it "example 2" do
# The value of my_variable is reused from the cache, not recalculated
expect(my_variable).to eq(some_expensive_calculation_result)
end
end
  • Let_it_be: This method is similar to let, but it’s designed for variables that should be set up before the example begins, ensuring they are available for the entire example context. Unlike let, let_it_be eagerly evaluates the value and sets it up once for each example, rather than lazily evaluating it on-demand. Use let_it_be when you want to set up the variable eagerly and ensure its availability for the entire example context.
RSpec.describe MyClass do
let_it_be(:my_variable) { some_expensive_calculation }

it "example 1" do
# The value of my_variable is set up before the example begins
expect(my_variable).to eq(some_expensive_calculation_result)
end

it "example 2" do
# The value of my_variable is still set up for this example
expect(my_variable).to eq(some_expensive_calculation_result)
end
end

Sometimes your code depends on external objects or services, and you will need to simulate the behavior of those objects without actually doing a full integration test, that is where mocks come in, to specifically focus on only the functionality that you need for your test and control the result that you get from the simulated object. Rspec has many helpers built for this purpose.

Instance or class double: these helper methods create doubles (mocks) for specific objects or classes. These doubles allow you to specify the methods you want to mock and define their return values or behavior.

# Mocking an instance of MyClass with the method "some_method"
my_instance = instance_double(MyClass, some_method: "mocked_result")

# Use my_instance as a stand-in for MyClass in your tests
allow(MyClass).to receive(:new).and_return(my_instance)

You can use the allow or expect to set up method stubs and verify method calls.

# Stubbing the method "some_method" of MyClass to return a specific value
allow(MyClass).to receive(:some_method).and_return("mocked_result")

# Using MyClass with the mocked behavior
result = MyClass.some_method
expect(result).to eq("mocked_result")
# Setting up an expectation for the method "some_method" of MyClass
expect(MyClass).to receive(:some_method).and_return("mocked_result")

# Using MyClass, which should trigger the expectation
result = MyClass.some_method
expect(result).to eq("mocked_result")

Double: It allows you to create a simple double without needing a specific class or object to base it on.

# Creating a basic double without any specific behavior
my_double = double("MyDouble")

# Use my_double as a mock in your tests
allow(MyClass).to receive(:some_method).and_return(my_double)

Instance or class spy: Similar to instance_double and class_double, RSpec provides instance_spy and class_spy methods to create spies, which are like doubles but also record any method calls made on them.

# Creating a spy for an instance of MyClass
my_instance = instance_spy(MyClass)

# Use my_instance as a spy in your tests
my_instance.some_method
expect(my_instance).to have_received(:some_method)

Webmock: This is a powerful library that allows you to stub HTTP requests at the HTTP level. It intercepts outgoing HTTP requests and responds with predefined data without actually making the real request.

# In your RSpec configuration or test setup
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)

# In your test
require 'net/http'

RSpec.describe 'API interactions' do
it 'should mock a request and response using WebMock' do
# Stub the HTTP request and response
stub_request(:get, 'https://api.example.com/data')
.to_return(status: 200, body: '{"key": "value"}', headers: { 'Content-Type' => 'application/json' })

# Your code that makes the API request
response = Net::HTTP.get_response(URI('https://api.example.com/data'))

# Test the response
expect(response.code.to_i).to eq(200)
expect(JSON.parse(response.body)).to eq({ 'key' => 'value' })
end
end

Remember that mocks should be used judiciously and only when necessary to isolate the code being tested. Overusing mocks can lead to brittle tests and make it harder to refactor the code. Always aim to strike a balance between using mocks and testing real implementations.

Photo by Fab Lentz on Unsplash

The final part of a test is the assertion, for this you are going to need to compare results to expected results and for this we use matchers. RSpec matchers are a fundamental feature of RSpec, and they are used to define the expected behavior of your code in test examples (specifications). Matchers allow you to write expressive and readable tests by providing a rich set of methods that compare the actual results of your code with the expected outcomes.

There are some basic matchers that you can use to describe the expected behavior of your code:

  • expect(actual_value).to eq(expected_value): Checks if actual_value is equal to expected_value.
  • expect(actual_value).to be(expected_value): Checks if actual_value is the same object as expected_value.
  • expect(actual_value).to be > 10: Checks if actual_value is greater than 10.
  • expect(actual_value).to be_within(0.1).of(3.14): Checks if actual_value is approximately 3.14 with a tolerance of 0.1.
  • expect(actual_value).to be_truthy: Checks if actual_value is truthy (not nil or false).
  • expect(actual_value).to be_falsey: Checks if actual_value is falsey (either nil or false).
  • expect(array).to include(element): Checks if array includes element.
  • expect(hash).to have_key(key): Checks if the hash has the specified key.
  • expect { code_that_raises_error }.to raise_error(ErrorClass): Checks if the code raises an error of the specified class.
  • expect { code_that_raises_error }.to raise_error(‘Error message’): Checks if the code raises an error with the specified message.

There are many more matchers that you can check on the rubydocs page or the Rspec documentation.

Thank you for reading this article, I hope it has helped you to understand a little bit more about Rspec and how tests help you improve your code. If you liked the article give it a clap and check out my other articles on different software development topics.

References

--

--

Mateo mojica
Mateo mojica

Written by Mateo mojica

Electronic engineer and software developer

No responses yet