Like most readers, I write lots of unit tests as I code. I don't do TDD, which requires writing the tests beforehand, but I often have two panes open in my IDE at the same time one with the class I'm coding and one in which I'm banging out the corresponding unit tests. This allows me to test my code immediately when I save and build. Right away, I can tell that my code works correctly or contains off-by-one errors and other gremlins. I find this way of proceeding very agreeable. I have confidence in my code; and as I build up the large set of tests, I know I'm not inadvertently disrupting my previous work or that of my colleagues.
However, after years of working this way, I find that many tests are often hard to read. It's not the code itself (JUnit has done a good job over the years of improving syntax), it's that I often need to read the entire test through to understand what's being tested. Moreover, tests frequently have specific set-up requirements that can make up a significant portion of the test before the actual crucial line checks an assert
statement. And that assert
statement is often not terribly informative.
A final difficulty is JUnit's suboptimal syntax for supporting parameterized tests. These are the tests in which you send lots of values to the same routine to make sure it provides the right response under a variety of inputs. For example, if you are parsing regular expressions and testing the error detection, you'd want to send a large number of incorrect combinations and make sure all are properly rejected. With JUnit, such a test requires a lot of choreography.
So for a while, I've been looking at how to make my tests more readable and easier to write. The solution I've been moving towards is to use BDD tools. BDD (behavior-driven development) has at its core a series of tests that specify what behavior a program should exhibit when a certain action takes place. The many BDD frameworks in use today favor a syntax that has a flow that's roughly: Given a situation, when an action occurs, then a specified set of results should ensue. For example:
given a bank balance of $100
when a withdrawal of $150 is attempted
then an error should be displayed and the balance should be $100
I write lots of these tests, too. While I should embrace the entire BDD philosophy, which prescribes translating all requirements and user stories into tests of this kind, I instead write these tests just before I start in on a given piece of work. They are higher-level than unit tests and often span multiple units. Ideally, they incarnate some activity that would normally be part of UATs.
Most BDD frameworks ride on top of JUnit, which enables easy integration into innumerable IDEs and workflows. As a result, it is entirely possibly to write unit tests using this kind of syntax. Over the last year, I've been doing more of this. And as the readability soars, I find that I've essentially forgone the old, classic JUnit syntax.
In my case, because I like writing unit tests in Groovy, I use a Groovy-based framework, called Spock. (There are many other options including easyb and Cucumber. They all do roughly the same thing and use similar syntax.) The choice of Groovy to write unit tests for Java code is probably worth a separate editorial, but is one of the most productive decisions I've taken. Groovy has many useful features that simplify writing tests. The two that leap to mind are optional typing, which facilitates creating test objects, and method injection. Groovy's metaprogramming capabilities enable me to inject a new method into any object on the fly. Powerful stuff!
Returning to Spock, here's an example of a parameterized test:
class HelloSpock extends spock.lang.Specification {
def "length of Spock's and his friends' names"() {
expect: name.size() == length
where: name | length
"Spock" | 5
"Kirk" | 4
"Scotty" | 6
}
}
Notice the test name is a string, which appears in any failure diagnostic. Already this is more readable than trying to use the JUnit convention of putting the explanation in an oversized Java method name. The test is clearly evident in the "expect" statement; the table of values in the "where" statement is clear, understandable, and trivial to write. The result of this elegant and quick syntax is that I often go overboard and create a table with every possible edge value. In the above example, for example, I would surely test an empty string as well as one that consisted only of spaces.
More-complex tests are also elegantly supported. For example:
def "should send messages to all subscribers"() {
when:
publisher.send("hello")
then:
1 * subscriber.receive("hello")
1 * subscriber2.receive("hello")
}
This test verifies that when a publisher sends a "hello" message, the two subscribers receive it exactly once. If they don't receive it or receive it more than once, the test fails. It's not hard to extend this snippet to express the expected behavior of mocks, is it?
I could go on; but the point is that, using this new syntax, my tests are far more readable, easier to write, and they invite me to include truly comprehensive testing at unit levels and higher.
— Andrew Binstock
Editor in Chief
[email protected]
Twitter: platypusguy