charles c. lee

charles c. lee

staff engineer at Shopify

11 May 2020

Simple Personal Rules to Structure Rspec Tests

RSpec is a popular testing framework for Ruby programs and it comes with its own helper methods and rules.

The mental checklist I have is to first write down the barebones structure of my tests. This has helped me plan out variables and the logic needed within my code.

  1. Split tests for methods using describe.
  2. Within each describe block, scope out the different situations that could happen using context.
  3. Determine how each context is different and break those out into variables using let.
  4. Define and override the default let with the context specific data.
  5. Write out the expectations of the tests.
class MySimpleModel
  def self.a_class_method
    ...
  end
  
  def an_instance_method
    if external_service.my_conditional?
      ...
    else
      ...
    end
  end
end

Using the above example, I can then write out the following specs based on the first couple steps of the checklist:

RSpec.describe MySimpleModel do
  describe '.a_class_method' do
    ...
  end

  describe '#an_instance_method' do
    context 'with external_service' do
      context 'when my_conditional? is true' do
        ...
      end

      context 'when my_conditional? is false' do
        ...
      end
    end
  end
end

I’ve broken out the class and instance method using the describe blocks. Then within my instance method’s test, I’ve also broken out the different situations that occur using contexts. In addition I’ve wrapped those context with another one explaining that there’s an external service in use.

RSpec.describe MySimpleModel do
  ...

  describe '#an_instance_method' do
    context 'with external_service' do
      let(:external_service) do
        instance_double('ExternalService', my_conditional?: my_conditional)
      end

      before do
        allow(ExternalService).to receive(:new).and_return(external_service)
      end

      context 'when my_conditional? is true' do
        let(:my_conditional) { true }

        ...
      end

      context 'when my_conditional? is false' do
        let(:my_conditional) { false }

        ...
      end
    end
  end
end

Now focusing on the instance test, I’ve added the variables that are shared (the external_service double) and have defined the variables that are different in my contexts (the my_conditional variable).

Now the stage is set to test the methods of this class and I can start to write out the expectations. Having fully scoped out the branches of logic and variables in use, it becomes clear what and how my code is being tested.

Alongside those steps in the checklist, I’ll reference BetterSpecs for other best practices when using RSpec. Happy testing!