Rule Of Thumb

RSpec examples should have enough detail in the descriptions to rewrite them from scratch.

Background

In the early days of automated testing there was some test code, which would exercise the implementation code, and make assertions about the results — something like:

def test_addition
  assert(UInt8.add(1, 2) == 3)
  assert(UInt8.add(255, 1) == 0)
  assert_raises(ArgumentError) { UInt8.add(1, 256) }
  assert_raises(ArgumentError) { UInt8.add(1, -1) }
end

Some time in the early 2000s a slightly different style of testing evolved called behaviour-driven development (BDD), which produced RSpec and Cucumber.

To oversimplify, what makes BDD different is the conceptual shift from writing tests to writing specifications (specs for short). A test is anything that exercises the implementation and makes assertions about the results, whereas a spec is a detailed description of desired behaviour. The test above might be written in RSpec like:

RSpec.describe UInt8 do
  it 'adds unsigned 8-bit integers' do
    expect(subject.add(1, 2)).to eq(3)
  end

  it 'wraps when addition results in an overflow' do
    expect(subject.add(255, 1)).to eq(0)
  end

  it 'raises an error when given an integer outside the unsigned 8-bit range' do
    expect { subject.add(1, 256) }.to raise_error(ArgumentError)
    expect { subject.add(1, -1) }.to raise_error(ArgumentError)
  end
end

It’s relatively common to see RSpec used to write tests, not specs. For example:

RSpec.describe UInt8 do
  it 'works' do
    expect(UInt8.add(1, 2)).to eq(3)
    expect(UInt8.add(255, 1)).to eq(0)
    expect { UInt8.add(1, 256) }.to raise_error(ArgumentError)
    expect { UInt8.add(1, -1) }.to raise_error(ArgumentError)
  end
end

Rationale

This rule of thumb — that each RSpec example description should contain enough detail to rewrite it from scratch — is one way to ensure that you’re writing specs, not just tests.

Imagine that the subject under test and all the example bodies were deleted. If the RSpec file was written in proper BDD style, we should be able to rewrite them based on the example descriptions alone. The implementation may not be exactly the same, but all of the desired behaviours should be there.

RSpec.describe UInt8 do
  it 'adds unsigned 8-bit integers'
  it 'wraps when addition results in an overflow'
  it 'raises an error when given an integer outside the unsigned 8-bit range'
end

But if the RSpec file is being used as a simple test, it would be impossible to know what the desired behaviours were.

RSpec.describe UInt8 do
  it 'works'
end

The purpose of BDD, and the intention behind RSpec, is to capture a specification of desired behaviour. So one reason to write detailed example descriptions in RSpec is that you’re using the tool the way it was intended to be used.

Without going too deep into the rationale behind BDD, there are a few other benefits.

When an example fails, a good description will tell you which is broken: the example or the implementation. Without an understanding of the intended behaviour, it’s easier to accidentally “fix” a failing example by making it pass when the implementation produces incorrect results. This is more likely to happen when the correct behaviour is ambiguous.

RSpec example descriptions also function as a kind of documentation between developers, communicating how parts of the codebase work. RSpec’s documentation formatter is evidence of this. If you’re writing specs in a BDD style, the output of this formatter should be an understandable list of behaviours…

> rspec --format documentation

UInt8
  adds unsigned 8-bit integers
  wraps when addition results in an overflow
  raises an error when given an integer outside the unsigned 8-bit range

Finished in 0.00149 seconds (files took 0.10574 seconds to load)
3 examples, 0 failures

… not a list of method names and “works correctly”.

UInt8
  #add
    adds correctly
  #subtract
    works