Made Tech Blog

Focus with well structured RSpec tests

Before joining Made, my experience with unit testing was always with PHPUnit. It’s very flexible in allowing you to write tests quickly; create a class, add some methods that start with test, include some assertions, and away you go. What I don’t think PHPUnit—and similar—allow you to do well is think about how to structure your tests, and what to focus them on. For that you have to rely on experience and good discipline.

Take the following example test in RSpec, written in ‘retro’ style:

describe Warehouse do
  context 'when turning warehouse power off' do
    it 'should turn all machines off' do
      warehouse = Warehouse.new(power: true)
      machine = Machine.new(name: 'Vending machine', power_switch: true)

      warehouse.machines << machine

      expect(machine.power).to be_true

      warehouse.power_off

      expect(warehouse.power).to be_false
      expect(machine.power).to be_false
    end
  end
end

The above example is quite a common way of writing tests regardless of what language or test framework you’re using. But there is a lot happening in that one test:

  • Description of what to test
  • Creation of objects that we’ll be using in our tests
  • Setup the state of the objects
  • Assertions

It’s a bit mashed together. What are we actually testing? What if we want to reuse objects between tests?

We actually have two subjects that we’re testing here. The Warehouse and the Machine objects within it. At this level of testing, these should really be broken out into their own test specs:

describe Warehouse::Building do
  let(:warehouse) { create(:warehouse_building, power: true) }

  subject { warehouse }

  context 'when switching off power' do
    before(:each) do
      warehouse.power_off
    end

    its(:power) { is_expected.to be_false }
  end

  context 'when switching power on' do
    before(:each) do
      warehouse.power_on
    end

    its(:power) { is_expected.to be_true }
  end
end

And the corresponding Machine test:

describe Warehouse::Machine do
  let(:machine) { create(:warehouse_machine) }

  subject { machine }

  context 'when switched on, and turning warehouse power off' do
    before(:each) do
      machine.warehouse.power_off
    end

    its(:power) { is_expected.to be_false }
  end

  context 'when switched off, and turning warehouse power on' do
    before(:each) do
      machine.power_switch = false
      machine.warehouse.power_on
    end

    its(:power) { is_expected.to be_false }
  end
end

Don’t read too much into what’s going on with the code example, the thing to take away is the structure of the test, and its readability.

Breaking down the structure

The test structure mirrors the Given-When-Then pattern, derived from Behaviour Driven Development.

  • Given: Setup your tests using let and before blocks
  • When: Define your test subject
  • Then: Use it and/or its to make your assertions

Setting the scene

Using let along with Factory Girl to setup our test subjects allows us to have sensible defaults in a separate factory file so that our tests remain cleaner. Having these stored in a let block allows for reuse between tests, where the value is cached after its first use, but not between our test assertions.

That means that we can use our let values when setting up our tests. And we do that using before blocks.

Why use a before block rather than just doing setup in a subject block? Simply because it reads better.

Rather than:

subject do
  setup_this
  and_that
  test_this
end

Do this:

before(:each) do
  setup_this
  and_that
end

subject { test_this }

Great Expectations

Once the tests are all setup and ready, it’s time for those assertions. Depending on your test subject, you might choose to use it or its.

When testing objects, it’s always good to use its as you can easily test multiple properties and methods by simply passing the name as the first argument.

its(:power) { is_expected.to be_false }

The above is basically doing this behind the scenes:

expect(subject.power).to be_false

If you have multiple assertions to make, then you’d have a lot of lines repeated that look similar, so its is a much clearer way of defining them.

For other data types, it might be a better fit. An array for example:

it { is_expected.to include('Apple') }

Try and keep the assertions as simple and as minimal as possible. If you find yourself writing a lot of assertions in a single context, perhaps try breaking them out into a separate context.

With the various RSpec helpers on offer, you’re able to be more disciplined in writing well structured tests, that read better, while remaining focused on small pieces of functionality. Consider the readability at all times, as your tests often turn out to be great documentation for others.

About the Author

Avatar for David Winter

David Winter

Senior Technology Adviser at Made Tech

Code, coffee, cake and Eurovision. In no particular order.