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
tospecify_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 assignednil
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.