Unit Testing Best Practices

by Bob McCune on December 9, 2006

I was recently compiling a list of Test-driven Development (TDD) “Best Practices” for my client project’s wiki site and thought this would probably be useful to the community at large. I’ve been an avid practitioner and proponent of test-driven development for a few years now and have found it to be one of the most important practices I’ve adopted. If practiced correctly, it will have a significantly positive impact on the design and quality of your code.

Without further ado, here is my list of key concerns to keep in mind while practicing TDD…

Test Behavior, Not Methods

It is important to write test cases that test how a client object would interact with the class under test. A common question asked is “should I write unit tests for private or package-private methods?”, and my general response is, no. Private methods should not be directly tested as they do not comprise an object’s publicly exported API and instead only exist to support its publicly observable behavior. This certainly isn’t a hard and fast rule. If a private method is sufficiently complex or would be difficult to fully test via its public methods it can make sense to test these directly. If you choose to do so you’ll need to write some reflection code to invoke these methods or use something like the utilities provided by the JUnit Add-ons project.

Apply the “Too Simple to Break” Rule

Although maintaining a high degree of code coverage across the test suite is important, striving for 100% coverage is not advisable. There is a point of diminishing returns that needs to be considered. A good rule of thumb is if a method is too simple to break, don’t write a unit test for it. The question of what constitutes “to simple to break” is certainly up to interpretation, however, it’s rarely useful to write a unit test for a method like the following:

Testing simple getter and setter methods is generally not advisable.

Use Object-Oriented Best Practices In Your Test Cases

It is important to view your test cases as first class code assets. The same design techniques you would apply when writing your production code should be applied to test cases as well. This means you should look for opportunities to refactor your test cases to pull up common functionality into a base class, remove duplicate code, decompose long methods, etc. Failing to do so will drive up the cost of change and will contribute to the test suite losing value over time.

Use Appropriate Method Names

This may seem obvious, but it is all too common to see test methods with the following signatures:

Instead, use method names that clearly describe the purpose of the test. Doing so makes your test cases more understandable and easier for the team to maintain. For instance:

Don’t Use Try/Catch Blocks to Catch Unexpected Exceptions

The keyword with this practice is unexpected. The JUnit framework automatically handles any exceptions that bubble up the call stack. It is important that you don’t circumvent this behavior by using a try/catch block around a test that should not throw an exception. Doing so can mask bugs that won’t be caught until the code is in production. For instance, consider the following implementation of the UserService interface:

There are two primary scenarios that need to be tested:

  1. A valid User instance is returned for the specified username.
  2. A UserNotFoundException is thrown for the specified username.

In the first scenario, no exception should be thrown so we should not attempt to catch it. Instead, we should declare, in the test method’s signature, that an exception is thrown. This will allow any exceptions to bubble up to the JUnit framework in the event that our setup code was incorrect and inadvertently caused this exception.

In the second scenario, we are explicitly checking for the case where an exception is thrown. This means we need to wrap this in a try/catch block and fail the test if the exception is not thrown.

The following test case implementation shows how to appropriately test these two scenarios. I’m using EasyMock to mock the interaction with my DAO.

Don’t Rely on Visual Inspection

The success or failure of a test should not be predicated on a developer “eyeballing” the console to determine the result. This means you shouldn’t use System.out.println() or Logger.debug() statements to determine your test’s success state. Instead, you should exclusively rely on JUnit’s various assertXXX() methods so the test can be run in an unattended, automated manner.

Use A Code Coverage Tool

It is often difficult to gauge how completely your test cases are exercising a piece of code. Failing to fully test all execution paths could result in you missing a critical application bug that causes a system failure. Coverage utilities can easily be incorporated into your Ant or Maven build processes which will generate a visual coverage report showing you precisely which lines of code were tested.

Here are a few open source tools I use for this purpose:
Cobertura
Emma
EclEmma

Use The Appropriate Tool for the Testing Scenario

JUnit provides a solid framework for unit testing in a Java environment, however, JUnit alone does not provide you all the capabilities you need to effectively test your code. It is important to find the right tool for the job. Luckily, the community has developed a wide variety of extension to JUnit that will help you test virtually any scenario you might come across.

Here are a few unit and integration testing tools I commonly use:
DBUnit
MockRunner
EasyMock
JMock
Selenium

That’s it for now. Hopefully this will provide you with some useful things to keep in mind while writing your test cases. If you have any additions to this list, please comment.