Improving RSpec Test Clarity With YAML Expectations
Nov 28, 2014
We’ve been working on a new text matching module that takes any user-supplied address
('123 Madeup Lane') and determines whether it matches against any given sample
text ('<b>123</b> Madeup Ln'). Our class is called AddressToTextMatcher
and the initializer accepts strings address and sample_text, then provides
a public exact_match? that returns a boolean value.
What makes this module interesting after going through a few rounds of Ping Pong
Pairing is that the magnitude of test code is vastly outpacing the production
code, because the matcher is mostly taking advantage of gsub and regex logic that can be
condensed into concise single lines whereas the tests are accounting for lots of
corner cases that we want to test discretely. The raw test code (no refactoring
yet) looks like this:
So far we have about 20 lines of production code and 150 lines of test code.
Generally you expect the two to more or less to approach each other over time as
the tests start benefiting from reuse opportunities, but in this case
my sense is that the gap will only widen as we discover and add corner cases
to the tests that only require minor changes to the matching logic. We could
try to condense the tests, accounting for multiple cases in each
one and adding shared examples to clean up the repetitive exercise/expect
lines at the end of every it block.
But I like having each case tested in relative isolation, so what I really want is a
more organized, succinct way to add a bunch of corner cases/contexts and keep
them organized easily without scrolling through piles of nested context blocks.
I want something like this:
So I set about hacking together something that would read YAML in this format
and generate the necessary RSpec contexts/expectations. This is what I came up
Here’s the retrospective:
The obvious win here is twofold. First, the spec for the matcher class now
looks like this:
That is great, and I get to use the YAML format described above to articulate
all of the test cases I want in a super readable and extendible format.
There are a few things that immediately come to mind as inherent setbacks.
First, I like using my vim-rspec plugin to run individual tests, which isn’t
possible without further modifications to YamlExpector. It also
isn’t an obvious win from a code magnitude perspective, since the new class is
around 50 lines and the YAML definitions still require 80 lines of (albeit much
more readable) declarations. And this non-traditional approach adds some
complexity and DSL where it isn’t absolutely necessary.
Obviously, the YamlExpector class will need some further
refactoring/abstraction/optimization to be reusable. Right now it is very
closely coupled with testing expectations of a single method on a single class,
so there is significant work yet to be done if we want to keep this strategy in
our test suite.
Overall I’m going to relish the new expectation declaration format and keep an
eye out for unintended consequences as we move forward with the module.