Respectable: scenario outlines for RSpec

Every so often I write an example in my specs that I'd like to run for several variations of input. As RSpec doens't support this workflow per se, I wrote the Respectable gem.

Take for example the following case where we need to generate the full name for a user given a first and last name:

The signature of the method is simple enough:

# app/models/user.rb
class User
  def full_name
    # some operation using the user's first and last name
  end
end

We start our TDD-engines and jot down our first spec:

# spec/models/user_spec.rb
describe User do
  describe '#full_name' do
    it 'concats first and last name' do
      expect(User.new(first: 'Foo', last: 'Bar').full_name).to eq 'Foo Bar'
    end
  end
end

Tada! Now that wasn't that hard, was it?

Well-Mannered Developers™ as we are, we instantly start thinking of some interesting other values for first and last:

  • what if a user's first name is missing?
  • what if a user's last name is missing?
  • how to handle casing for last names like 'Van der Foo'?

Sure enough, we can create examples for all these cases:

# spec/models/user_spec.rb
describe User do
  describe '#full_name' do
    it 'concats first and last name' do
      expect(User.new(first: 'Foo', last: 'Bar').full_name).to eq 'Foo Bar'
    end

    it 'works when first name is nil' do
      expect(User.new(first: nil, last: 'Bar').full_name).to eq 'Bar'
    end

    it 'works when last name is nil' do
      expect(User.new(first: 'Foo', last: nil).full_name).to eq 'Foo'
    end

    # etc....
  end
end

Now this is the point where I become unhappy fairly quickly:

  • I have to come up with a separate it-block (with a description) for every variation.
  • I'm constantly repeating the expectation.
  • But most importantly: this code poorly shows the original intent.
    Namely to show how the method under test handles different values. It would require careful reading for others (i.e. my older self) to actually see what they are.

Sure we could shorten the expectations by using some helper-method, or put all expectations in a single it-block, but that would not improve the situation that much (and the repetition is still there).

Whenever I ran into this situation before I tried to extract the varying values from the code, like this:

# spec/models/user_spec.rb
describe User do
  describe '#full_name' do
    [
      ['Foo', 'Bar', 'Foo Bar'],
      ['Foo', nil, 'Foo'],
      [nil, 'Bar', 'Bar'],
    ].each do |first, last, expected|
      it "yields #{expected.inspect} given first: #{first.inspect}, last: #{last.inspect}" do
        expect(User.new(first: first, last: last).full_name).to eq expected
      end
    end
  end
end

While this makes the input-variations more prominent, it doesn't look elegant (less euphemistically speaking: it's downright ugly): both the collection of variations and the example-description look messy and we are introducing an extra level of nesting.
And this is just for the situation where we only have two parameters and an expected result; imagine needing to extent the example to use three or more parameters.

Then it struck me that Cucumber actually has something like this built-in in the form of scenario outlines. It allows you to run a scenario with different inputs:

Scenario Outline: eating
  Given there are <start> cucumbers
  When I eat <eat> cucumbers
  Then I should have <left> cucumbers

  Examples:
    | start | eat | left |
    |  12   |  5  |  7   |
    |  20   |  5  |  15  |

If only I could have this for my specs!

Meet Respectable

The Respectable gem aims to give you something like scenario outlines for RSpec.
Long story short: using Respectable the above example would look like this:

# spec/models/user_spec.rb
describe User do
  describe '#full_name' do
    specify_each(<<-TABLE) do |first, last, expected|
    # | first | last    | full        |
      | Foo   | Bar     | Foo Bar     |
      | Foo   | `nil`   | Foo         |
      | `nil` | Bar     | Bar         |
      | Foo   | Van Bar | Foo van Bar | # casing!
    TABLE

        expect(User.new(first: first, last: last).full_name).to eq expected
      end
    end
  end
end

Let's take a closer look at this spec:

  • we no longer have multiple (similar) expectations.
    There's just one block that gets evaluated for every row in the ASCII-table; it's clear what the intent of the code is.
  • we don't need to specify an it-block.
    For every row in the table an it-block is automatically generated. We don't have an extra level of nesting in our code this way.
    Inputs are easily added and removed: editing the ASCII-table is all it takes.
  • we don't need to come up with a description for every case.
    Every generated it-block gets a default description assigned. As an author I can focus instead on thinking up edge-cases. The output for the above example would look like this:

    User
      #full_name
        yields "Foo Bar" for first: "Foo", last: "Bar"
        yields "Foo" for first: "Foo", last: "`nil`"
        yields "Bar" for first: "`nil`", last: "Bar"
        yields "Foo van Bar" for first: "Foo", last: "Van Bar"
    

    A custom description can also be provided:

    # use the parameters passed to the block
    desc = 'full_name(%{first}, %{last}) => %{expected}'
    specify_each(<<-TABLE, desc: desc) do |first, last, expected|
    

    If you rather have the default RSpec description just pass desc: nil to specify_each.

  • use backticks if you need values other than string.
    By default every cell in the ASCII-table is turned into a stripped string. By using `nil` in the example above, we ensure that the block parameters gets assigned nil and not an empty string.
  • we can add comments.
    Anything following a # will be ignored. This helps to document the table or specific rows.

Summary

When a spec would benefit from testing different variations in the input, Respectable gem might be a nice tool in your toolbox. It allows you to denote the variations in a descriptive and easily readable manner.