Better RSpec Stubs

RSpec is a very powerful tool for testing applications. It's been around for a while and provides a rich feature set.

One thing that RSpec provides is the ability to stub a method on an object or class so that, instead of executing and behaving as it normally would, it instead returns a hard-coded value and never actually executes.

Here is a simple example [1]:

student = load_student_from_database(id: 77)
allow(student).to receive(:grades).and_return(['A', 'A+', 'C'])

puts(student.grades)

The second line in that snippet of code causes any calls to student.grades to always return the array ['A', 'A+', 'C']. The actual definition of the #grades method will never be invoked.

Let's build on this example.

We can imagine that if there exists a Student class in our system, then there probably exists a GpaCalculator class. Let's go ahead and TDD such a class into existence.

As a reference point, let's use the GPA scale of my Alma Mater, DePaul University, where an A translates to 4.0 grade points and a B translates to 3.0 grade points.

Here is what the first few tests might look like:

describe GpaCalculator do
  it 'calculates one A to be 4.0' do
    student = Student.new
    allow(student).to receive(:grades).and_return(['A'])

    expect(GpaCalculator.new(student).calculate).to eq(4.0)
  end

  it 'calculates one B to be 3.0' do
    student = Student.new
    allow(student).to receive(:grades).and_return(['B'])

    expect(GpaCalculator.new(student).calculate).to eq(3.0)
  end

  it 'calculates one A and one B to be 3.5' do
    student = Student.new
    allow(student).to receive(:grades).and_return(['A', 'B'])

    expect(GpaCalculator.new(student).calculate).to eq(3.5)
  end
end

[2]

This is already getting a little clunky. Someone who sees an opportunity for refactoring this may clean it up to look a little bit like this:

describe GpaCalculator do
  let(:student) { Student.new }
  subject { GpaCalculator.new(student) }

  it 'calculates one A to be 4.0' do
    allow(student).to receive(:grades).and_return(['A'])

    expect(subject.calculate).to eq(4.0)
  end

  it 'calculates one B to be 3.0' do
    allow(student).to receive(:grades).and_return(['B'])

    expect(subject.calculate).to eq(3.0)
  end

  it 'calculates one A and one B to be 3.5' do
    allow(student).to receive(:grades).and_return(['A', 'B'])

    expect(subject.calculate).to eq(3.5)
  end
end

That's certainly better. Unfortunately, many people will stop here and move on to other things. Now, for an example this small, it's not a grave sin to leave it as is. But on a large test suite, cleaning as much as you can is a big deal and really helps increase maintainability in the long term.

In a situation such as this, where you repeatedly stub the same method to return a different array (or hash) in every test, there exist a few techniques that remove noise and improve readability.

One approach that I've recently begun suggesting is this:

describe GpaCalculator do
  let(:grades) { [] }
  let(:student) { Student.new }
  subject { GpaCalculator.new(student) }

  before :each do
    allow(student).to receive(:grades).and_return(grades)
  end

  it 'calculates one A to be 4.0' do
    grades << 'A'

    expect(subject.calculate).to eq(4.0)
  end

  it 'calculates one B to be 3.0' do
    grades << 'B'

    expect(subject.calculate).to eq(3.0)
  end

  it 'calculates one A and one B to be 3.5' do
    grades << 'A'
    grades << 'B'

    expect(subject.calculate).to eq(3.5)
  end
end

A little cleaner, isn't it? We move the noisy syntax for "stub this to return that" out of each individual test and into a before block. What remains is the meat of every single test case–the expected GPA and the grades that directly affect it.

Some folks may not like the before :each block. An alternative is to throw the stub directly into the corresponding let block:

  let(:student) {
    student = Student.new
    allow(student).to receive(:grades).and_return(grades)
    student
  }

Which, in my opinion, isn't really any better, but it is an option.

A completely different approach would be use doubles[3] and avoid the allow altogether:

  let(:student) { instance_double('Student', grades: grades) }

Once you go through all this, your final version will probably look something like:

describe GpaCalculator do
  let(:grades) { [] }
  let(:student) { instance_double('Student', grades: grades) }
  subject { GpaCalculator.new(student) }

  it 'calculates one A to be 4.0' do
    grades << 'A'

    expect(subject.calculate).to eq(4.0)
  end

  it 'calculates one B to be 3.0' do
    grades << 'B'

    expect(subject.calculate).to eq(3.0)
  end

  it 'calculates one A and one B to be 3.5' do
    grades << 'A'
    grades << 'B'

    expect(subject.calculate).to eq(3.5)
  end
end

Voilà!

Remember, although your tests aren't production code and don't run your business, they're still a critical part of your business. If they're hard to read, modify, or add to, then development will suffer. Be on the lookout for ways to refactor them and make them more maintainable.


[1] Although this article uses RSpec and Ruby as an example, the idea presented here should be applicable to other languages and their respective test frameworks.

[2] For the sake of the example, we inject Student into the GpaCalculator here. A better approach would be to simply pass the student's grades into the GpaCalculator, and completely eliminate the dependency of GpaCalculator on Student. With this design your GpaCalculator stands alone and is usable in other places of your code where you don't necessarily have a Student object in your hands.

[3] instance_doubles, a.k.a verifying doubles, were introduced in RSpec 3 and provide some safety against stubbing methods that don't actually exist.

Dariusz Pasciak, Software Craftsman

Dariusz Pasciak is a former 8th Light employee.

Interested in 8th Light's services? Let's talk.

Learn more about our Ruby services

Contact Us