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:
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:
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 AssertionFailedError
.
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.
As you can see, the matcher is supposed to indicate success by returning true
and failure by returning false
.
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 wrapped_assertion
.
He started by explaining the situation and refreshing our memory about the semantics.
We promptly threw out some possibilities:
fail_when
, fail_on
, 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.
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:
So the matcher either matches or not, and returns true or false to indicate that.
The presence of should
or 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 assert_equal
and assert_not_equal
.)
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:
There's repetition there—the parallel structure of match
and 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 match
.
Now the example is beautifully simple:
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.