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:
public String getName() {
return name;
}
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:
public void testGetUser1() {
}
public void testGetUser2() {
}
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:
public void testGetUserWithInvalidUsername() {
}
public void testGetUserThrowsExpiredCredentialsException() {
}
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:
public class UserServiceImpl implements UserService {
private UserDao dao;
public User getUser(String username)
throws UserNotFoundException {
User user = dao.getUser(username);
if (user == null) {
String msg = "No User found for: '" + username + "'";
throw new UserNotFoundException(msg);
}
return user;
}
public void setUserDao(UserDao dao) {
this.dao = dao;
}
}
There are two primary scenarios that need to be tested:
- A valid
Userinstance is returned for the specified username. - A
UserNotFoundExceptionis 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.
public class UserServiceImplTest extends TestCase {
private UserDao dao;
private MockControl ctrl;
private UserServiceImpl service;
protected void setUp() throws Exception {
ctrl = MockControl.createControl(UserDao.class);
dao = (UserDao) ctrl.getMock();
service = new UserServiceImpl();
service.setUserDao(dao);
}
public void testGetUserWithValidUserName()
throws Exception {
User expected = new User();
String validUser = "validUser";
ctrl.expectAndReturn(dao.getUser(validUser), expected);
ctrl.replay();
User result = service.getUser(validUser);
assertSame(expected, result);
ctrl.verify();
}
public void testGetUserThrowsUserNotFoundException() {
String bogusUser = "bogusUser";
ctrl.expectAndReturn(dao.getUser(bogusUser), null);
ctrl.replay();
try {
service.getUser(bogusUser);
fail("Should have thrown a UserNotFoundException");
} catch (UserNotFoundException e) {
// should be here
}
ctrl.verify();
}
}
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.
3 responses so far ↓
1 x // Dec 11, 2006 at 8:01 pm
Not a word on TestNG?
2 Ricky Clarkson // Dec 11, 2006 at 10:04 pm
Why do test frameworks typically rely on exceptions? I wondered how C-based implementations work, and found that they tend to emulate exceptions.
I rolled my own, where each test merely returns true or false, and so far I haven’t seen any disadvantage with this. To be more clear, I could make it return an enum constant, SUCCESS or FAILURE, but it would be fairly equivalent.
Also, do any of the test frameworks allow for running multiple tests in parallel?
3 Bob McCune // Dec 13, 2006 at 5:27 am
Ricky, I can’t comment on the pros/cons of your approach as I don’t know the details. A framework like JUnit relies on a variety of assertions which gives you a fair degree of flexibility in determining and indicating why a test failed. Maybe your solution provides this flexibility, but I’d at least want some more control over how and why the test failed.
Leave a Comment