Today I Learned: How to Write DRY and Maintainable Tests with RSpec

When writing tests in RSpec, it’s crucial to keep your test suite DRY (Don’t Repeat Yourself) and maintainable. RSpec provides several powerful features to help achieve this, including shared examples, it_behaves_like, subject, and before hooks. In this post, we’ll explore how to use these features to write clean and effective tests.

Shared Examples in RSpec

Shared examples in RSpec allow you to encapsulate common test logic that can be reused across different contexts. This reduces duplication and makes your test suite easier to maintain.

Defining Shared Examples

Let’s start by defining shared examples. Suppose we have an Order model that we want to test for various states such as completed, pending, and canceled.

1
2
3
4
5
6
7
8
9
10
11
# spec/support/shared_examples/order_status_examples.rb

RSpec.shared_examples 'order status' do |status, expected_message|
  let(:order) { Order.new(status: status) }

  subject { order.status_message }

  it "returns the correct status message" do
    expect(subject).to eq(expected_message)
  end
end

In this shared example:

  • status and expected_message are parameters.
  • We create an order with the given status.
  • The subject block defines what we are testing, which is the status_message method of the order.
  • The it block asserts that the status message is as expected.

Using it_behaves_like to Include Shared Examples

Next, we include these shared examples in our tests using it_behaves_like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# spec/models/order_spec.rb
require 'rails_helper'
require 'support/shared_examples/order_status_examples'

RSpec.describe Order do
  context 'when the order is completed' do
    it_behaves_like 'order status', 'completed', 'Your order is completed.'
  end

  context 'when the order is pending' do
    it_behaves_like 'order status', 'pending', 'Your order is pending.'
  end

  context 'when the order is canceled' do
    it_behaves_like 'order status', 'canceled', 'Your order has been canceled.'
  end
end

Using subject and before Hooks

The subject and before hooks help in setting up your tests cleanly and consistently.

  • subject: Defines the main object under test.
  • before: Contains setup code that runs before each example.

Example

Here’s a simplified example to demonstrate the usage of subject and before.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RSpec.describe User do
  subject { described_class.new(name: 'Jane Doe', age: 30) }

  before do
    subject.save
  end

  it 'is saved successfully' do
    expect(subject).to be_persisted
  end

  it 'has the correct name' do
    expect(subject.name).to eq('Jane Doe')
  end

  it 'has the correct age' do
    expect(subject.age).to eq(30)
  end
end

Advanced Example with Multiple States

To show how these features can work together in a more complex scenario, let’s consider a Project model with various states and properties.

Defining Shared Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# spec/support/shared_examples/project_status_examples.rb

RSpec.shared_examples 'project status' do |status, is_active, is_completed|
  let(:project) { Project.new(status: status) }

  subject { project }

  before do
    subject.save
  end

  it "is active: #{is_active}" do
    expect(subject.active?).to eq(is_active)
  end

  it "is completed: #{is_completed}" do
    expect(subject.completed?).to eq(is_completed)
  end
end

Using Shared Examples in Tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# spec/models/project_spec.rb
require 'rails_helper'
require 'support/shared_examples/project_status_examples'

RSpec.describe Project do
  context 'when the project is active' do
    it_behaves_like 'project status', 'active', true, false
  end

  context 'when the project is completed' do
    it_behaves_like 'project status', 'completed', false, true
  end

  context 'when the project is inactive' do
    it_behaves_like 'project status', 'inactive', false, false
  end
end

Conclusion

By using shared examples, it_behaves_like, subject, and before hooks, you can write DRY and maintainable tests with RSpec. These tools help you encapsulate common logic, reduce duplication, and set up your tests cleanly.

Happy testing!


[alex_rocha] by alex

🇧🇷Senior Software Developer