In Progress
Unit 1, Lesson 21
In Progress

RSpec Focus Tests with Erin Dees

Have you ever hesitated to re-run tests after making a change, because you don’t want to re-run all of your tests? In today’s episode, you’ll learn how to use RSpec’s “focus test” feature to only run the tests you currently care about.

Video transcript & code

Your guest chef today is Erin Dees. Erin is the co-author, along with Myron Marston, of the book Effective Testing with RSpec 3, published by The Pragmatic Bookshelf. This book teaches you to get the most out of your test suite by focusing on the value that your tests deliver. I was a technical reviewer for this book, and spoiler alert: it's good!!

Hailing from sunny Portland, Oregon, Erin is also a coach and competitor at the sport of racewalking. And if you've never looked up videos of racewalking on YouTube… well, you should.

Here's Erin to talk to you about focusing tests in RSpec.


You've heard a lot about the importance of fast feedback when you run your tests. Even if you have a large test suite that takes a long time to finish, you can still get rapid feedback while you're coding or debugging. The secret is to run only the tests you need to.

In Episode 53, Avdi showed you how to narrow down your test run to specific filenames, line numbers, and spec names. In this episode, we're going to talk about one more way to optimize how you run your specs: focusing.

Let's say we're testing a Ruby class that helps us keep track of our board game collection. Over time, our test suite has accumulated several different examples that check this class's behavior. First, we expect that a new GameShelf starts out empty.

RSpec.describe GameShelf do
  let(:shelf) { GameShelf.new }

  context 'with no games' do
    it 'is initially empty' do
      expect(shelf).to be_empty
    end
  end
end

Then, we'll see what happens when we add board games to the shelf. As a convenience, we'll define a couple of games here that we can use in the next few examples.

RSpec.describe GameShelf do
  # ...

  let(:dixit)   { { name: 'Dixit',   rating: 10.0 } }
  let(:qwirkle) { { name: 'Qwirkle', rating: 8.0 } }
end

With the help of these definitions, we can check that a game stays on the shelf once we've added it.

RSpec.describe GameShelf do
  # ...

  let(:dixit)   { { name: 'Dixit',   rating: 10.0 } }
  let(:qwirkle) { { name: 'Qwirkle', rating: 8.0 } }

  context 'with no games' do
    it 'remembers the games I add' do
      shelf.add(dixit)
      expect(shelf.games).to include(dixit)
    end
  end
end

We've spelled out how an empty game shelf should behave. Now, let's talk about a shelf that already has games on it. First, we'll create a new context that fills up our shelf before each example runs.

RSpec.describe GameShelf do
  # ...

  context 'with multiple games' do
    before do
      shelf.add(dixit)
      shelf.add(qwirkle)
    end
  end
end

Inside this context, we can write an example that searches the shelf for our favorite game.

RSpec.describe GameShelf do
  # ...

  context 'with multiple games' do
    before do
      shelf.add(dixit)
      shelf.add(qwirkle)
    end

    it 'knows my highest-rated game' do
      expect(shelf.highest_rated).to eq(dixit)
    end
  end
end

Now that we have a few specs written, let's take a look at the class under test. In a typical project layout, we'd keep this code in its own file named game_shelf.rb.

require 'spreadsheet_store'

class GameShelf
  def initialize
    @games = SpreadsheetStore.new
  end
end

Internally, this GameShelf class uses a comma-separated value, or CSV, spreadsheet to remember what games we have. This format makes it easy to save our games to a file and then load them into just about any other program.

Here's how a GameShelf instance adds new games to the spreadsheet:

require 'spreadsheet_store'

class GameShelf
  # ...

  def add(game)
    row = [game[:name], game[:rating]]
    @games.append_row(row)
  end
end

Over the course of a single test run, we'll end up calling the spreadsheet's append_row method several times.

As our project grows, we may find we're spending a lot of time in this method and waiting too long for our specs to finish. Or we may see test failures for certain edge cases we can't yet explain.

In these situations, it can be handy to see what's going on in the underlying library code. For this exercise, we'd open spreadsheet_store.rb, and add either a binding.pry breakpoint…

class SpreadsheetStore
  # ...

  def append_row(row)
    binding.pry
    # puts 'DEBUG: inside append_row'
    @rows << row
  end
end

…or some debugging output, inside the implementation of append_row.

class SpreadsheetStore
  # ...

  def append_row(row)
    puts "DEBUG: appending row #{row.inspect}"
    @rows << row
  end
end

But now, we've got a problem. This method gets called so frequently that if we run our entire test suite, we'll end up hitting our debugging code many times.


$ bundle exec rspec

GameShelf
  with no games
    is initially empty
DEBUG: appending row ["Dixit", 10.0]
    remembers the games I add
  with multiple games
DEBUG: appending row ["Dixit", 10.0]
DEBUG: appending row ["Qwirkle", 8.0]
    knows my highest-rated game

Finished in 0.00536 seconds (files took 0.07672 seconds to load)
3 examples, 0 failures

Our test report is cluttered with debugging output. And if we're using breakpoints, we've had to navigate out of them over and over again. This kind of friction really gets in the way of a seamless problem-solving experience.

It would be nice to run just a single example, so that we only hit our debugging code once. That's exactly what RSpec's focusing feature lets us do: it helps us focus our attention on a single example or group of examples.

To see how this option works, let's turn our attention back to our specs. (I've collapsed the example bodies to make it easier to see what we're changing.)

RSpec.describe GameShelf do
  context 'with no games' do
    it 'is initially empty' do
    end

    it 'remembers the games I add' do
    end
  end

  context 'with multiple games' do
    it 'knows my highest-rated game' do
    end
  end
end

We'll focus on the first example by adding the letter f to the beginning of the word it. You can think of fit as standing for "focused it."

RSpec.describe GameShelf do
  context 'with no games' do
    fit 'is initially empty' do
    end

    it 'remembers the games I add' do
    end
  end

  context 'with multiple games' do
    it 'knows my highest-rated game' do
    end
  end
end

You may feel that the word fit seems really terse here; I'm inclined to agree, but I think it's okay in this situation. We're making a conscious choice to favor quickness over clarity. We can easily toggle an example or group on or off by changing just a single character.

We wouldn't want to commit this kind of focused example into revision control. If we needed to disable test code for longer than a quick experiment, we'd mark the code with pending or skip instead.

Once RSpec is properly configured (which I'll show you how to do in a moment), we can see that it runs only the example we focused on.


$ bundle exec rspec
Run options: include {:focus=>true}

GameShelf
  with no games
    remembers the games I add

Finished in 0.00191 seconds (files took 0.09074 seconds to load)
1 example, 0 failures

You can also focus on an entire example group by using fcontext in place of context

RSpec.describe GameShelf do
  # ...

  fcontext 'with no games' do
    it 'is initially empty' do
    end

    it 'remembers the games I add' do
    end
  end

  context 'with multiple games' do
    # ...

    it 'knows my highest-rated game' do
    end
  end
end

…or fdescribe in place of describe.

RSpec.fdescribe GameShelf do
  # ...

  context 'with no games' do
    it 'is initially empty' do
    end

    it 'remembers the games I add' do
    end
  end

  context 'with multiple games' do
    # ...

    it 'knows my highest-rated game' do
    end
  end
end

To enable this focusing behavior of RSpec, you'll need to do a bit of one-time setup. Inside your spec_helper.rb file, enable the filter_run_when_matching option.

RSpec.configure do |config|
  config.filter_run_when_matching :focus
end

If you've previously set up your project by running the rspec --init command…


$ bundle exec rspec --init
  create   .rspec
  create   spec/spec_helper.rb

…RSpec has already added this setting for you, inside a commented-out section of recommended options.

# ...

RSpec.configure do |config|
# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.
=begin
  # This allows you to limit a spec run to individual examples or groups
  # you care about by tagging them with `:focus` metadata. When nothing
  # is tagged with `:focus`, all examples get run. RSpec also provides
  # aliases for `it`, `describe`, and `context` that include `:focus`
  # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
  config.filter_run_when_matching :focus

  # ... other useful settings here...
=end
end

You can enable focused specs, plus a bunch of other useful settings, by removing the beginning and end of this comment block.

# ...

RSpec.configure do |config|
# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.
  # This allows you to limit a spec run to individual examples or groups
  # you care about by tagging them with `:focus` metadata. When nothing
  # is tagged with `:focus`, all examples get run. RSpec also provides
  # aliases for `it`, `describe`, and `context` that include `:focus`
  # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
  config.filter_run_when_matching :focus

  # ... other useful settings here...
end

Each setting has a comment block above it explaining the benefit that it provides for you.

Before we sign off, let's take one more look at the test output when you're focusing on a single example:


$ bundle exec rspec
Run options: include {:focus=>true}

GameShelf
  with no games
    is initially empty

Finished in 0.00195 seconds (files took 0.08166 seconds to load)
1 example, 0 failures

See that bit about run options? RSpec is running only the tests whose :focus property is set to true. RSpec calls these kinds of properties tags, or more generally, metadata.

When you focus on a single example by changing the word it to fit

RSpec.describe GameShelf do
  context 'with no games' do
    fit 'is initially empty' do
    end
  end

  # ...
end

…you're using shorthand built into RSpec to set that example's :focus tag. It's just as if you'd set the property explicitly.

RSpec.describe GameShelf do
  context 'with no games' do
    it 'is initially empty', focus: true do
    end
  end

  # ...
end

You're not limited to using only the built-in metadata and shorthand that come with RSpec. You can define your own. In part 2 of this series, we'll do just that.

Until then, happy hacking!

Responses