I’m currently reading the excellent Growing Object-Oriented Software, Guided by Tests. Although I have read less than half of it so far, this book has already changed the way I think about Test-Driven Development. It covers a broad range of TDD topics and their intersection with OO, but for now I want to focus on a very small part of this landscape – the naming of unit tests.
Names are important. Class names, method names, variable names – good names inform us, bad names mislead. I’m sure we all know that naming stuff can be tricky. However, a little discipline in choosing names can go a long way to improving the quality of unit tests.
Consider a tiny class I wrote recently while performing the leap year kata:
public class Year { public Year(int year) { // constructor type stuff } public boolean isLeapYear() { // code to check for leap year } }
I could have made isLeapYear()
static, and in fact I have done so in previous attempts. But it isn’t the design choices of the interface to Year
that I want to discuss.
So, we know what we want to achieve with Year
, and we also know what the interface to that class looks like. Great. Now we can write a failing unit test. First naming choice – the test class itself. YearTest
(or YearTests
, if you prefer) doesn’t seem too controversial. One thing to watch out for is the renaming refactoring; some IDEs may not be smart enough to automatically change the name of the test class if we rename Year
. Still, no biggie. So far, so good.
Some people don’t like to, but we’ll start with a degenerate case. It doesn’t make sense to have a year like -1984, so let’s make Year
reject negative years with an exception. We could write:
@Test (expected = IllegalArgumentException.class) public void firstTest() { new Year(-1); }
We can figure out the intent of the test, and thus learn something about the intended behaviour of Year
, by reading the test body and the expectation in the annotation. However, ideally we should be able to glean all of that just from the name of the test, and we can’t do that from “firstTest
“. If you name your tests like this, stop reading for a moment and punch yourself in the face. Repeatedly. You deserve it.
You’re back? Good. Probably (hopefully) your face has remained blissfully unpunched, because most people care enough to avoid such an obviously poor name. If you really can’t think of a better name initially, maybe because the purpose of the test is not yet completely clear, you can always come back to it later and rename when it is.
Often I see tests with names that begin with the prefix test
. Unless your environment uses a tool like TestDox, the test
prefix is redundant and should be avoided. I think this is usually a hangover from JUnit 3, where tests had to be named that way to make the test runner pick them up. JUnit 4 replaced this mechanism with the @Test
annotation.
So what is a better name for firstTest
? Until recently, I would have gone with something like:
@Test (expected = IllegalArgumentException.class) public void constructorThrowsExceptionForNegativeYear() { new Year(-1); }
The name constructorThrowsExceptionForNegativeYear
describes the method under test and its expected behaviour in a specific scenario. If the test fails, we can see straight away what that failure means. There is plenty of support for this approach, and indeed, it is a vast improvement on the previous attempt.
Let’s assume we’ve made that test pass. Using the same naming scheme, the next test could be:
@Test public void isLeapYearReturnsFalseForTypicalCommonYear() { Year year = new Year(1985); assertFalse( year.isLeapYear() ); }
I generally try to avoid long method names, but I consider this less important for test methods. They are only ever called by the test runner, so they don’t appear anywhere else in the code base and nobody has to type them. Besides, a descriptive but long name is always preferable to a less descriptive short one.
We still have the same minor refactoring problem we mentioned earlier; if we rename isLeapYear()
, the IDE may not automatically rename our tests appropriately. Still, apart from that, we seem to be doing quite well. The test name indicates which method of Year
is being tested, under what conditions, and what we expect its behaviour to be. Not too shabby.
We could continue our red-green-refactor cycle and end up with these test methods:
public void constructorThrowsExceptionForNegativeYear() public void isLeapYearReturnsFalseForTypicalCommonYear() public void isLeapYearReturnsFalseForAtypicalCommonYear() public void isLeapYearReturnsTrueForTypicalLeapYear() public void isLeapYearReturnsTrueForAtypicalLeapYear()
I have written many a test using this naming approach. However, I often had a nagging doubt whenever I needed a test that did not map onto exactly one method. I always ended up temporarily abandoning the naming scheme out of sheer necessity. This happened pretty frequently, and sometimes caused me to wonder if I had written the “wrong” test, or else made a poor interface design choice in the target class.
For example, imagine we were writing java.util.TreeSet
. One of the distinguishing features of TreeSet
is that it maintains its elements in their “natural” order (as opposed to a random order, or the order in which they were added). To test that, we might do something like:
@Test public void whatDoWeCallThisTest() { TreeSet<String> treeSet = new TreeSet<String>(); treeSet.add("b"); treeSet.add("c"); treeSet.add("a"); assertInAlphabeticalOrder( treeSet.iterator() ); } private void assertInAlphabeticalOrder(Iterator<String> it) { String previous = ""; while ( it.hasNext() ) { String current = it.next(); String failMsg = String.format("%s should be alphabetically greater than %s", current, previous); assertTrue(failMsg, current.compareTo(previous) > 0); previous = current; } }
The problem is immediately apparent – which method of TreeSet
is being tested here? Is it add()
? Is it iterator()
? This brings us to a point made by Steve Freeman and Nat Pryce in the book:
Test names should describe features of the target class, not methods.
If we were to describe TreeSet
in terms of a list of its features, it would look like:
TreeSet: - maintains elements in natural order - does not store duplicate elements - rejects objects of incomparable type - etc.
This list can guide us to the tests we need. We are now thinking about what the target class does, not how it does it. We need a test for each feature, and we should name each test according to the feature it exercises. Revisiting the above TreeSet
example using this approach gives us a test name of maintainsElementsInNaturalOrder()
.
Let’s return to YearTest
and refactor by applying this naming scheme:
public void rejectsNegativeYears() public void recognisesTypicalCommonYears() public void recognisesAtypicalCommonYears() public void recognisesTypicalLeapYears() public void recognisesAtypicalLeapYears()
I mentioned TestDox earlier. TestDox uses JUnit test names to create a feature list for the target, so for Year
we would see:
Year: - rejects negative years - recognises typical common years - recognises atypical common years - recognises typical leap years - recognises atypical leap years
One of the benefits of unit tests is that they teach us about the code being tested. They show us the API and how to use it; they tell us what behaviour to expect. Test names contribute a lot more to this description of behaviour when we write and name them based on features, and seeing them extracted into a list like this emphasises that point.
Agile development is incremental in nature. We add features to a system one at a time, and at a lower level, we add features to a class one at a time. Writing and naming our unit tests based on features is highly compatible with this incremental, one-feature-at-a-time approach.
Further Reading
http://googletesting.blogspot.co.uk/2007/02/tott-naming-unit-tests-responsibly.html
http://pragprog.com/magazines/2011-04/test-abstraction
http://agileinaflash.blogspot.co.uk/2012/05/seven-steps-to-great-unit-test-names.html
Leave a Reply