Monday, February 20, 2017

Test-as-spec and assertion syntax



I like to say that tests should, first and foremost, be a human-readable spec. Let's look at what that can mean for how we write assertions.

Suppose we're writing a card game, and we want to assert that a deck of cards is sorted the way you'd find them when you first open the box. (I'm using this simple example as a proxy for the kinds of more complex problems that we see in legacy code. It's up to you to map these ideas to that context.)

An approach I see in a lot of code is to iterate over the cards to assert. Perhaps something like:


This kind of code makes it obvious that an AssertEquals would be valuable, so that on failure you can see the expected and actual values in the test results.

If this test fails, you only know about one incorrect card. If there are more, you won't know until you fix the current error and rerun the test.

A richer assertion library might offer AssertSorted. It could even take a set of 1 or more sort key selectors. The result might look like:


(That's C++ lambda syntax, if you haven't seen it before).

Both of these approaches are "computer science" solutions - they work in the solution domain, and use the language of computer code. If I want my test to be a human readable spec, I need to use the language of the problem domain. I could take a step in that direction by extracting a method, giving:


But we're also doing TDD. In TDD, we want the tests to give us feedback about the design of the code. And this test is saying "the notion of being sorted that is missing from the code under test". Taking an intuitive leap, the class that should hold that notion is a "deck of cards", which is also missing from the code under test. That leads to:


I like the improvements to the design of the code and the way the test reads, but I am sad to lose the ability to provide a detailed report when this assertion fails. I'm not sure how I would fix that, or if it would ever actually matter.

It's interesting to me that we're back to the bool-only assertion from the first example.

1 comment:

Unknown said...

I like the second version:

AssertSorted(cards,
[](auto& _) { return _.value; },
[](auto& _) { return _.suit; });

You seem to be missing the name of the test, which I would call testNewDeckSortedBySuitThenValue

Part of the reason I like this version is it highlights a mistake. The sorting is in the wrong order (value, suit would be Aces, Twos, Threes, etc...)

But the maybe larger issues is who wrote AssertSorted?
It doesn't feel like this should be a framework level assert (although, possibly as it is fairly generic).
At least, I wouldn't expect this assert to be provided out of the box.
However, this seems like a perfect case for a:

Custom Assert

Just because the framework doesn't provide it is not reason not to have this in your code. I feel the biggest problem in unit tests is programmers tend to restrict themselves to the language of the unit testing framework.

It should be noted this happens a lot less in BDD, because the 'Then' statements tend to be reused which is a type of custom assert.