Last week I had a great time at the Latin America Rails Summit. (Thanks to the organizers for inviting me!) During breaks in the action here and there, I spent time working on Tarantula, and for a lot of that time I was sitting with Chad Fowler and David Chelimsky. David was working on RSpec, and asked Chad and me for help coming up with a good name for a method. The ensuing conversation struck all of us as a great example of the power of good names.
It's often very difficult to find just the right name for something in a program. It's also hard to understand why names are so important. Names are just symbols, after all, and in some sense they're arbitrary. Certainly the computer doesn't care what name you use for something, as long as you're consistent. And programmers are fairly good at dealing with arbitrary names; think of all the acronyms we use without ever thinking of what the letters stand for, and repurposed words like "bus," "mask," and "string," with technical meanings that bear little resemblance to the original English meanings.
So if we can deal with non-optimal names just fine, and choosing good names is really difficult, it hardly seems worth it. But good programmers understand the value of names. This story illustrates how hard it can be to find just the right name, and also how good names can help you to understand your own program and domain in deeper ways.
David was working on RSpec's built-in DSL for defining matchers. A matcher in RSpec is the object that actually tests a particular condition to determine whether or not it holds, along with a method that's used to select the desired matcher. There was a time when creating a matcher was a bit cumbersome, but David and other RSpec contributors have made it much easier over time, and part of that is a little DSL. Here's an example, from the RSpec docs:
Spec::Matchers.define :be_in_zone do |zone| match do |player| player.in_zone?(zone) end end
That snippet creates a matcher called
be_in_zone that checks whether a supplied player (the actual value, in traditional unit-testing parlance) is in the expected zone.
With that matcher defined, a programmer can write an expectation in this form:
In some situations, RSpec needs to work with assertions defined for test/unit.
That works just fine;
when using RSpec with Rails, for example, you can use Rails' custom assertions like
assert_select and it just works.
But RSpec users would like to be able to easily wrap an assertion in an RSpec matcher, so that their tests are more consistent.
That's the feature David was working on: support in the matcher DSL for calling a test/unit assertion and mapping that to RSpec semantics. The matcher needs to call the assertion, determine the result, and report that result the way an RSpec matcher is supposed to.
David wrote the first version without thinking about names very much (just to get it working) and came up with the
wrapped_assertion method, used like this:
Spec::Matchers.define :equal do |expected| match do |actual| wrapped_assertion(AssertionFailedError) do assert_equal expected, actual end end end
Both test/unit and RSpec recognize three results of a check: success, failure, and error.
Success is when everything happens as expected.
An error occurs when the code under test (or perhaps the test itself) raises some unexpected exception.
Failure, on the other hand, happens when the code all seems to work (in the sense of not raising any exceptions) but one of the expectations is not met.
In test/unit, failure is indicated when an assertion raises
So two of the three situations are indicated when an exception is raised;
the distinction lies in which exception.
The definition of
wrapped_assertion is explicit about that:
it takes an argument that is the exception class representing failure.
def wrapped_assertion(error_to_rescue) begin yield true rescue error_to_rescue false end end
As you can see, the matcher is supposed to indicate success by returning
true and failure by returning
Any unexpected exception just bubbles out of the method, indicating an error.
When that was working, David checked it in and asked Chad and me for our thoughts about a better name than
He started by explaining the situation and refreshing our memory about the semantics.
We promptly threw out some possibilities:
fail_on_raise ... I don't remember all of the suggestions,
but I know those were in the mix.
And I think we settled on
fail_when_raises, because that made sense even if the parameter was optional and omitted:
fail_when_raises will fail when any exception is thrown, whereas
fail_when_raises(AssertionFailedError) will fail only when that explicit error is thrown.
That was that, we thought. It was dinner time, so we closed our laptops and forgot about the problem.
Keeping the Domains Straight
The next day, however, we found ourselves around a different table, back to working on our three different projects. David realized he wasn't happy with the method name, for a subtle reason.
This is a method that bridges two domains: the test/unit domain and the RSpec domain. Inside the method, test/unit rules: we call an assert method and interpret the result. Outside the method, however, is the domain of RSpec. The method name needs to be expressed in RSpec terminology that's appropriate for the situation.
The test/unit terminology of success, failure, and error does apply to RSpec, but not at the level of matchers. A match expression is not a complete expectation in RSpec; it only constitutes an expectation when matched with a should or should_not, like this:
actual.should match(expected) # or actual.should_not match(expected)
So the matcher either matches or not, and returns true or false to indicate that.
The presence of
should_not indicates whether that match was expected, and translates it to success or failure.
(This allows a single matcher to handle both positive and negative expectations, as opposed to test/unit assertions, which tend to come in positive/negative pairs such as
Method names including the words "fail" or "succeed" were letting the test/unit domain leak out of the method body.
A better name would be expressed in RSpec terminology.
We toyed with
false_when_raises until Chad suggested
match_unless_raises and we all knew it was right.
At that point we all thought we were done, and I mentioned that it would be good to blog about the whole dialogue; many programmers don't take names very seriously, and it would be good to show the real experience of three good programmers spending more than 5 minutes, in two separate conversations spread across two days, on a name for one simple method.
But the big payoff was yet to come. Once the correct name was in place, it helped David to see something important about his problem domain.
Let's look at that original example again, now with the new name:
Spec::Matchers.define :equal do |expected| match do |actual| match_unless_raises(AssertionFailedError) do assert_equal expected, actual end end end
There's repetition there---the parallel structure of
match_unless_raises---that is a clue to something wrong.
David noticed it immediately and realized that
match_unless_raises should be promoted to the same level of the API as
Now the example is beautifully simple:
Spec::Matchers.define :equal do |expected| match_unless_raises(AssertionFailedError) do |actual| assert_equal expected, actual end end
That's all it takes to write a matcher that wraps a test/unit assertion.
A lot has been written about the importance of names in programming, including Tim Ottinger's classic Ottinger's Rules for Variable and Class Naming. Good names help us communicate with other programmers, and with our customers. The notion of the ubiquitous language that features so prominently in domain-driven design is based largely on the choice of good, meaningful, shared names.
But good names also reflect the clarity of our thinking, and help us to communicate with ourselves about our own thoughts. And when we get the names right, our code and the problem domain can speak clearly to us, revealing accidental complexity and the underlying simplicity we're searching for.
Good names are worth the trouble.