Writing good unit tests, Part 2: Follow your nose

Assertions, exceptions, and refactoring in test code

Klaus Berg continues his in-depth investigation of tools and best practices for programming with GUTs. Get tips for writing cleaner and more efficient assertions with the help of a matching library (Hamcrest) and handling checked and unchecked exceptions in JUnit 3 and 4 and TestNG. The remainder of the article focuses on refactoring test code, first explaining why it's important and then revealing the three layers of code smell that indicate something's rotten. This article is packed with tips and trivia for the code quality addict. Level: Intermediate

The first half of this article presented best practices for writing test code, both from a technical angle and from a process-oriented one. Code coverage tools can help you measure how well your tests perform with respect to executing the different statements and branches of your system under test. In this article I continue the discussion with a look at two foundations of quality test code: writing proper assertions (e.g., with Hamcrest), and handling checked and unchecked exceptions (in JUnit 3.8.x, JUnit 4, and TestNG). I'll also present an example class that should give you ideas for dealing with assertions and exceptions in callback methods.

The second half of the article will focus on refactoring test code, from answering the question "Why refactor test code?" to outlining the various code smells you're likely to encounter in your unit tests. I'll also discuss tools that are specifically helpful for detecting code smells (most of them introduced in the first half of this article) and will conclude with some code quality standards for knowing when your test code is "good enough."

About the sample code

The source code package that accompanies this article contains an Eclipse project with source code and corresponding test code that demonstrates some of the points to be discussed. If you download the code, you'll find the structure below the root directory Eclipse_Example as follows:

  • The src directory contains example source code and a JUnit3 extension called AssertExceptions that can be used to test exceptions in an object-oriented fashion with JUnit 3.8.x.
  • The test directory contains the corresponding test code written with JUnit 4 and JUnit 3. Some test code makes use of the Hamcrest assertion library and contains a customized Hamcrest assertion file (FileMatchers.java).
  • build.xml contains an Ant script to create an Emma test coverage HTML report in the reports directory.
  • ant_junit.xml contains an Ant script to run the unit tests and to create an HTML JUnit 4 report in the junit directory.
  • XmlConfigBuilder.java, located in the src/javaworld/junit4/example/log4j directory, is a Log4j XML config file merger that merges arbitrary combinations of profiles but ignores their appender part. The appender part comes from a master XML file that contains appender definitions only. This file can be created using the following command:
    new XmlConfigBuilder(..).createAppenderFile()
    
    By running XmlConfigBuilderAPITest, you can see different configurations merged into one final config file. If you have different log-level settings, the highest one will survive.
  • The resources/templates directory contains a set of Log4J XML configurations that can be merged. appenders.xml, located in this directory, contains all available appenders.

The code was created and tested on Windows XP SP3 with JDK 6, Ant 1.7, and Eclipse 3.4.

Assertions in JUnit and TestNG

Assertions are a cornerstone of unit testing, enabling developers to test assumptions about code as it's being written. Each assertion contains a boolean expression that you believe will be true when the assertion executes. If it evaluates to false, the system or your test framework will throw an error. So, assertions are used to determine and "report" a test case verdict. As old as they are -- dating back to the early days of JUnit -- assertions aren't perfect, and they're not always written as well as they could be. On the other hand, writing good assertions isn't rocket science. (Well, unless of course you're NASA.) So, in this section, we'll look at simple ways to improve your assertions in JUnit and TestNG, with the help of a smart library called Hamcrest.

In JUnit 3 (the first widely used open source testing framework) assertions are built using the Assert class. Depending on the version of JUnit you use, Assert is either located in the package junit.framework (JUnit 3) or in org.junit (JUnit 4). You don't need to make explicit use of the classname Assert in either version; JUnit 3's TestCase extends Assert, and in JUnit 4 you can use Java 5's static import capabilities, like so:

import static org.junit.Assert.*;

TestNG allows you to write assertions by means of the assert statement that has been available since JDK 1.4. So you can write code like this:

myInstance.doSomething(1,2) : "comment if the test fails"

TestNG also lets you use JUnit's asserts, which are included in the class org.testng.AssertJUnit. Because JUnit 3.x asserts are hard to read (the parameter order is counterintuitive for English-speakers), TestNG introduces another class, Assert, in its org.testng package. The only way this is different from JUnit's class is that the message-string parameter is always the last parameter, and the expected and actual values are reversed, as shown in Listing 1.

Listing 1. Different Assert classes in TestNG

@Test
public void checkAccounts() {
   ...
    // JUnit Assert
   org.testng.AssertJUnit.assertEquals("The two accounts should be the same", expected, actual);

    // TestNG Assert
   org.testng.Assert.assertEquals(actual, expected, "The two accounts should be the same");
}

If you do not provide an explicit error message, the default problem report handling in either JUnit or TestNG is quite limited. You will only get a message telling you that an AssertionError has occurred. How can you do better? The best way is to use a kind of DSL to write proper and easily readable assertions. The Hamcrest library, now hosted by the Google code repository, can assist you with this goal.

1 2 3 4 5 Page 1