If any of the following are true of your unit tests, you might not be following some simple guidelines for effective unit testing:

  • I can't test that because it is static/void/private.
  • My unit tests take too long to run.
  • My test fails if the database/webservice/system is down.
  • My unit test's Spring configuration file is out of sync with production.

Unit testing can be fairly simple to implement with immeasurable value for both the customer and the implementing team. Unfortunately more often than not, basic principles and tools that can help prevent these problems are not being applied.

What follows are proven guiding principles to successful unit
testing.

You can quickly jump to one of the specific sections with the links below:

Unit test basic recipe:

Create an @Before annotated method.
In this
method:

  • Create the class under test
  • Inject any dependencies into the class under test (preferrably mocks / stubs)

Example:

@Before 
public void setup() throws Exception {
 testClass = new ClassUnderTest();
 
 // Create any mocks/stubs/dependencies 
 // if not already Created via annotations 
 // Now inject the dependency 
 testClass.setDependency(mockDependency); 
} 


All @Test annotated methods should throw Exception

Example:

@Test 
public void simpleTest() throws Exception {} 


When testing for an expected exception, wrap in a try/catch and add a fail immediately after the call that should fail:

Example:

@Test 
public void expectedException () throws Exception { 
 try { 
 testClass.someFailure();
 fail("Expected MyException was not thrown"); 
 } 
 catch (MyExceptino ex) {} 
} 


Fail test cases for unexpected exceptions:

  • NEVER catch general exception.
  • Always allow the test method to throw exception. This immediately fails the test case.

Example:

@Test public void happyPath() throws Exception { 
 // If exception occurs on this call, 
 // test will fail and show exception. 
 testClass.goodMethod(); 
} 


Don't forget to test for the unexpected:

  • Test for nulls
  • Test for exceptions
  • Test for empty collections
  • Test for negative values
  • Test for interesting date boundaries (December 31st / January 1st)


Use the right assertion for the job.

  • assertEquals is to compare equality and is often over used
  • assertTrue/assertFalse are for testing boolean values.
  • There are richer sets of assertions available which can be used for more literate programming and better failure messages.

Example:

// Don't do this 
assertTrue(myValue == null); 
 
// Do this 
assertNull(myValue); 

Back to Top

Consider using a Matcher framework for more literate assertions:

The assertion "assertThat" allows for matchers:

  • Traditional assertions use "verb" "object" "subject"
    • "assert equals 12 x"
  • Think "subject" "verb" "object"
  • "assert that x is 12"
  • assertThat syntax: assertThat(msg, value, matcher(expectedValue))
  • assertThat + Matchers makes assertions more readable


Use Hamcrest for matchers: http://hamcrest.org/

  • Hamcrest is a simple matcher framework
  • It's statically imported into your class
  • It provides a rich set of matchers

Example:

ListForecastDay> forecasts = weatherWS.getForecasts("23238");
 
// Test for non-empty list.
assertThat(forecasts, notNullValue()); 
assertThat(forecasts, hasSize(greaterThan(0)); 
 
// test for item in list
assertThat(forecasts, hasItem(expectedForecast));
 
// Evaluate and test contents of the list. 
for (ForecastDay dayForecast : forecasts) { 
 assertThat(dayForecast.zipCode, equalTo("23238"));
 assertThat(dayForecast.getLow(), lessThan(dayForecast.getHigh()); 
}


Back to Top

Dependency Injection

Decouple unit tests from external resources via stubs/mocks

  • Do not maintain unit test specific Spring configuration.
    • Don't use SpringJUnit4ClassRunner for unit tests (it's ok for integration tests)
    • Special spring test dependency context files are brittle and never stay in sync with your application's context

Back to Top

Mocking and Frameworks:

Prefer Mocks: Stubs vs Mock

  • A Stub:
    • Requires coding and implementation of the stub for each permutation
    • Returns a pre-programmed value
  • A Mock:
    • Uses a mocking framework
    • Can capture and validate input values provided as parameters to a dependent call.
    • Can validate the number of invocations to a mocked call
    • Can easily create exception flows.


Testing Behavior with Mocks:

  • Mocked tests measure how your class under test uses it's dependencies.
  • This is an important and valuable distinction from traditional unit testing.
  • With traditional unit testing approaches you have no good way to unit test "void" returns.
  • Recommendation: Use Mocks instead of stubs because they provide better insight into your class under test's behavior, and less code.


Prefer Strict to Nice Mocks:

  • Nice Mocks:
    • Provide "natural" return values for methods (Object: null, numeric: 0, boolean: false)
    • Will not fail a test case for unexpected calls.
    • These are occasionally useful if behavior is not important to the test.
  • Strict Mocks:
    • Expectations must be set for any call to mocked object.
    • Tests will fail if an unexpected behavior/call is encountered.
      • This is a GOOD thing.
      • It provides a "canary in a coal mine" to detect behavioral changes in your application.
  • Recommendation: Since Mocks help to validate behavior, prefer strict mocking approaches to nice mocks.


Use a mocking framework:

Back to Top

Tying it all together. A simple example:

Consider the following simple method:

public void sendMessage(MyModel model) throws MyAppException { 
 Connection connect = null;
 
 if (model != null) { 
 try { 
 connect = connectionManager.getConnection(); 
 connect.transmit(model); 
 } 
 catch (Throwable ex) { 
 throw new MyAppException("Something broke!", ex);
 } 
 finally { 
 if (connect != null) 
 connect.close(); 
 } 
 } 
} 


To properly unit test this we should create the following test cases:

  1. Model's value is null:
    • Expectation: No errors, connection manager was never used
  2. Model exists, no errors sending message:
    • Expectation: connection manager and connection used, message sent, and connection closed
  3. An exception occurs during message transmission:
    • Expectation: connection manager used, exception is caught, a new MyAppException is thrown, and connection is closed.


What follows is a complete example test with PowerMock using the EasyMock dialect to test the exception flow (test case 3 above). Don't worry if you're unfamiliar with PowerMock or EasyMock at this time. There are copious comments to guide you through.

You will see all of the concepts covered throughout this article captured in the implementation of this test case.

import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.easymock.PowerMock;
import org.powermock.api.easymock.annotation.Mock;
import org.powermock.modules.junit4.PowerMockRunner;
 
@RunWith(PowerMockRunner.class)
public class TestMessageSender {
 
 @Mock // Create mock by annotation
 private ConnectionManager mockCM;
 
 private MessageSender messageSender;
 
 /**
 * Create the class under test and inject any mock dependencies
 * @throws Exception
 */
 @Before
 public void setup() throws Exception {
 messageSender = new MessageSender();
 messageSender.setConnectionManager(mockCM);
 }
 
 /**
 * Using mocks, create an exception on the connection's 
 * transmit() method.
 * Expectations: 
 * - Connection manager was used to retrieve a connection 
 * - An exception is caught and re-thrown as a MyAppException 
 * - Connection is closed.
 * 
 * @throws Exception
 * Anything unexpected that gets through will cause the 
 * test case to fail.
 */
 @Test
 public void sendMessageExceptionTest() throws Exception {
 
 // Define objects required for testing. Exception
 expectedEx = new Exception("Expected Exception");
 
 MyModel model = new MyModel();
 
 // We need a mock connection. This isn't a dependency
 // and is needed for this test case so we'll create it locally.
 // This demonstrates how to create the mock in code.
 Connection mockConnection = PowerMock.createMock(Connection.class);
 
 // define expected mock behaviors
 // Behavior: Connection Manager should return a connection
 expect(mockCM.getConnection()).andReturn(mockConnection);
 
 // Behavior: connection should throw an exception 
 // on the transmit() call
 mockConnection.transmit(model);
 expectLastCall().andThrow(expectedEx);
 
 // Behavior: connection should always be closed (finally block)
 mockConnection.close();
 expectLastCall();
 
 // Prepare the mocks to start capturing behavior as they are used.
 PowerMock.replayAll();
 
 // Execute the test, and verify the expected exception was thrown.
 try {
 messageSender.sendMessage(model);
 
 // If we get here, the exception didn't get thrown... that's a
 // failure.
 fail("Expected MyAppException to be thrown");
 } catch (MyAppException ex) {
 // Validate that the exception we caught contains the proper
 // internal exception.
 assertThat(ex.getCause(), instanceOf(Exception.class));
 
 Exception internalException = (Exception) ex.getCause();
 assertThat(internalException, sameInstance(expectedEx));
 }
 
 // Validates that all expected behaviors occurred,
 // and no unexpected ones occurred.
 PowerMock.verifyAll();
 }
}

Back to Top

Final Thoughts

On many projects, unit tests are incomplete, if not skipped entirely because they're seen as too difficult to create, or too time consuming to write and execute. This perceived difficulty is a result of not being able to peel away the inter-dependent classes to focus on the unit of code we want to test.

Following the basic recipes covered in this article coupled with some simple complimentary frameworks like Hamcrest and one of the mocking tools, you can create readable, focused, true unit tests. The behavior based testing mindset that mocking introduces helps to provide a "canary in a coal mine" set of unit tests that can signal unexpected or unintended changes in your code base. This allows you and your team to "refactor mercilessly" with the confidence that the consequences will be well understood.