Clean up your RSpec tests with ease

Use "subject" to explicitly define the value that is returned by its call

  • Development
  • Digital Agency
  • RSpecs
  • Ruby
  • Testing

When starting with your first programming language, your thoughts about automatically testing your code with programmatic tests might be almost zero. At Fintory, we are a huge fan of testing your code programmatically. And NO, we don't do TDD (Test-driven Development), because of specific reasons – we are writing the tests after we tested the code manually and verifying that everything is working.

However, in each case (TDD or DTT – Development Then Tests) we are in the need of a huge test suite that works pretty solid to ensure that the test does also not fail when we are going to change any part of the codebase.

Avoid waterfalls

This premise often results in test suits being too much overwhelming and including a lot of duplicated code, which makes the test suite a bad pig to look at.

For example, have a look at the following code snippet:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # frozen_string_literal: true require 'rails_helper' RSpec.describe Api::V1::TrainingsController do render_views describe 'GET #show' do describe 'is free' do let(:training) { create(:training) } it 'returns with status code 200' do get(:show, params: { format: :json, id: training.id }) expect(response.status).to eq(200) end it 'returns videos of the training' do get(:show, params: { format: :json, id: training.id }) expect(json['videos'].instance_of?(Array)).to be(true) end it 'returns materials of the training' do get(:show, params: { format: :json, id: training.id }) expect(json['materials'].instance_of?(Array)).to be(true) end end end end

This code snippet is a pain in our eyes, which our devs needed to refactor (what, refactor tests?!) some time ago.

Simplification made easy

With RSpec we have a nice block-scope variable called subject. It can be used to define a "subject" which can then be called with its own name in the RSpec examples.

Let's see how we are able to simplify the above tests with the usage of subject

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # frozen_string_literal: true require 'rails_helper' RSpec.describe Api::V1::TrainingsController do render_views describe 'GET #show' do describe 'is free' do subject { get(:show, params: { format: :json, id: training.id }) } let(:training) { create(:training) } it 'returns with status code 200' do subject expect(response.status).to eq(200) end it 'returns videos of the training' do subject expect(json['videos'].instance_of?(Array)).to be(true) end it 'returns materials of the training' do subject expect(json['materials'].instance_of?(Array)).to be(true) end end end end

As you can see, using the nifty subject helper can be very nice to clean up the tests. However, we can go a little further, since there is a nice side-effect in the scoping of the blocks in RSpec/Ruby.

Going a little further

Since we are using blocks in plain Ruby and due to the nature of RSpec that every example is a simple block, we are able to make use of this.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # frozen_string_literal: true require 'rails_helper' RSpec.describe Api::V1::TrainingsController do render_views subject { get(:show, params: { format: :json, id: training.id }) } describe 'GET #show' do describe 'is free' do let(:training) { create(:training) } it 'returns with status code 200' do subject expect(response.status).to eq(200) end # [...] end describe 'is paid' do let(:training) { create(:training, :paid) } it 'returns with status code 200' do subject expect(response.status).to eq(200) end # [...] end end end

What have we done?

  1. We moved the subject to the top-level of the tests.
  2. We created a new describe block for paid content.

So what?

We mentioned the blocks before, and since we are not defining the let once, but per example, we are now able to use the subject for each example, but without redefining it in each describe block since it is scoped and will work with the defined let from the respective describe block.

We are using a different training instance for the "is paid" block than the "is free" block, but the subject is still working as before. For all examples. BOOM!

Wrapping up

Surely, we need to check where to put the subject, and what to expect from it. We also had cases, where every example was different from the other. In those cases, extensive usage of subject does not make sense at all. However, we always have some cases, where the usage of it makes absolute sense in our eyes, and to have a clean test suite is always good.

If you don't want to mess around with the tests, you can ask us to write the tests for you or even build the complete product for you. We are always happy to help.

Dominik Schmidt

CTO

Subscribe to our newsletter

No spam, just insightful content on design, development, product management, AI and much more.

Subscribe
I have read and accept the Terms of Service & Privacy Policy.