Java Unit Test with TestNG
Terminology
- CUT (Class Under Test) – a class in the production code which is being tested
- MUT (Method Under Test) – a method in the production code which is being tested
- Fixture – the set of fields defined in a test class and initialized in
setUp()
for use in multiple tests
Frameworks
While you will not be arrested by the Unit Testing Police for not following the policies below, you will be helping the team by ensuring uniformity and consistency:
- Use TestNG
- This is the prevailing framework and easily supports test groups to facilitate integration and other kinds of tests
-
Although TestNG allows you to use JUnit style assertions, it is less confusing to simply stick with TestNG style everywhere
-
Use Mockito
-
Although some existing tests were written with EasyMock, you should find that Mockito can easily do everything that EasyMock does but with much less code
-
- Avoid PowerMock
- While PowerMock can coexist with TestNG and Mockito, the features it provides enable developers to work around untestable code, rather than making the code more testable
-
Use Hamcrest
-
This is primarily relevant to special assertions using
assertThat()
-
Workflow
If you follow a TDD methodology, write the tests first which express the features you wish to implement. Otherwise, make a first pass at writing the code under test, then proceed to the list below.
- You can use your IDE to generate at least one test per method in the CUT
- In IntelliJ, select the class name and press
ALT-Enter
, then select "Create Test" - It is usually useful to have a
setUp()
method, so check this box - Select all the methods
- In IntelliJ, select the class name and press
- Mark each test class with
groups = "unit"
, like so:-
@Test
(groups =
"unit"
)
public
class
MyClassTest
{
...
}
-
- Name your test "methodUnderTest_Conditions" or "methodUnderTestConditions" or "testMethodUnderTestConditions"
- Prefer the names from left to right, depending on the PMD/Checkstyle rules in effect
- This scheme makes it easy to see what test and method failed during test failures
- Sometimes it is convenient to also append the expected output value to the name, like:
equals_WrongClass_False()
- Write a test for every code path through each method
- For each method, there should be at least as many tests as the cyclomatic complexity of the MUT
- If you know that an expression inside the method under test throws, then you should write a test for that scenario
- Every nullable parameter should have a test for both the
null
and not-null
conditions- The expected behavior of the method under a null parameter should be clearly documented in the Javadoc for the method
- If the above is not true, then add the appropriate Javadoc
-
/**
* Returns the list of hotel summaries for the IDs provided. Does not return null.
*
* @param hotelIds the list of hotel IDs for the summaries to fetch--must not be null
*/
public
List<Summary> getHotelSummaries(List<String> hotelIds);
// Or possibly:
/**
* Returns the list of hotel summaries for the IDs provided, or null if hotelIds is null or no data is available.
*
* @param hotelIds the list of hotel IDs for the summaries to fetch
*/
public
List<Summary> getHotelSummaries(List<String> hotelIds);
- If the expected behavior of a test is for the MUT to throw an exception, then use the
expected
annotation for TestNG:-
@Test
(expected = InvalidArgumenException.
class
)
public
void
testGetHotelSummariesNullHotelIDsThrows()
{
this
.retriever.getHotelSummaries(
null
);
}
-
- If an object is constructed the same way in several tests, hoist it to the test fixture
- Move the local variable to a field of the test class
- If the value is immutable, make it
static final
and initialize it directly - Otherwise, initialize it in
@BeforeMethod public void setUp()
-
@Test
(groups =
"unit"
)
public
class
HotelRetrieverTest
{
private
LodgingContentRetriever contentRetriever;
private
ReviewRetriever reviewRetriever;
private
HotelRetriever hotelRetriever;
@BeforeMethod
public
void
setUp()
{
this
.contentRetriever = mock(LodgingContentRetriever.
class
);
when(
this
.contentRetriever.getContentSummary(any(), any(), any()))
.thenReturn(
new
ContentSummary(
1234
,
"Hotel Name"
, ...);
this
.reviewRetriever = mock(ReviewRetriever.
class
);
when(
this
.reviewRetriever.getReviewSummary
this
.hotelRetriever =
new
HotelRetriever(
this
.contentRetriever,
this
.reviewRetriever);
}
}
-
- Minimize mocking noise
- The mocks become much more readable if you use static imports
when/then
clauses are easier to read when thethen
clause is wrapped:-
when(retriever.getHotelSummaries(any()))
.thenReturn(...);
-
-
Use good assertions
-
assertEquals()
is usually the best assertion, because it is the strongest (most precise) assertSame()
is appropriate for testing fluent APIs and normalizing/interning caches-
Avoid
assertNotNull()
, which is often the weakest assertion–try to find a reliable property of the result which can be asserted instead -
If there are no good built-in matchers for your assertion, use Hamcrest's
assertThat()
and write the appropriate custom matcher
-
- Ensure the test can fail
- Confirm this by first writing the test in a way that fails
- Try to fail the test by only modifying the test input(s) or expected output(s), not the assertions
- It is the assertion which must fail, not the whole test, per se
- If an assertion can never fail, then it is a useless NOP, and the test itself provides false coverage
-
// HotelRetriever.java
public
List<Summary> getHotelSummaries(List<String> hotelIds)
{
final
List<Summary> result =
new
ArrayList<>();
// result is never reassigned
...
return
result;
}
// HotelRetrieverTest.java
@Test
public
void
testGetHotelSummaries()
{
// Useless assert, because it can never fail
assertNotNull(retriever.getHotelSummaries(HOTEL_IDS));
}
-
- Confirm this by first writing the test in a way that fails
-
Do not use reflection to violate privacy
-
A common pattern uses Spring's
ReflectionTestUtils
to make a private field accessible for override in a test– DO NOT DO THIS!!! -
Instead, make the field an injected dependency using c'tor injection:
-
// Bad
public
class
HotelRetriever
{
private
static
final
Logger LOGGER = LoggerFactory.getLogger(HotelRetriever.
class
);
...
}
public
void
testGetHotelSummaries()
{
final
Logger logger = mock(Logger.
class
);
ReflectionTestUtils.setField(retriever,
"LOGGER"
, logger);
...
verify(logger).info(containsString(
"message"
));
}
// Good
public
class
HotelRetriever
{
private
final
Logger logger;
...
public
HotelRetriever()
{
this
(LoggerFactory.getLogger(HotelRetriever.
class
));
}
// Package private for testing
HotelRetriever(Logger logger)
{
this
.logger = notNull(logger);
}
}
public
void
testGetHotelSummaries()
{
final
Logger logger = mock(Logger.
class
);
final
HotelRetriever retriever =
new
HotelRetriever(logger);
...
verify(logger).info(containsString(
"message"
));
}
-
- Or, add a [package private] getter if necessary
-
- Test methods which throw only need to declare
throws Exception
- More precise
throws
clauses are useless because nobody except the test runner should be calling the test method - The thrown exceptions should be exercised in separate tests via
@Test(expected = ThrownException.class)
- More precise
- Use code coverage metrics to show you where tests are missing
- Code coverage only tells you where you are not done – it does not tell you when you are done !!!
- It does not guarantee that your tests make strong assertions – only manual inspection can verify this!
- It does not know whether you have tested boundary conditions – only careful analysis can verify this!
Guidelines
- Tests must be parallelizable with no synchronization
- This also implies that the test runner must be able to run them sequentially in any order and succeed
- Both of the above conditions means that tests may only share immutable state!
- All mutable state must be constructed at test initialization time (mostly in
setUp()
) - Code which relies on 3rd-party singletons should mock the singletons to satisfy this requirement
- If the CUT is a singleton, it should be implemented in a way which allows multiple instances for testing (usually by providing a package private c'tor)
- Tests must be repeatable
- Ideally, all code executed in a test should be observationally deterministic–each step should produce the same output, as far as the caller is concerned
- This means that non-deterministic elements must be mocked, if possible (this includes the clock/calendar!)
- Each test should only verify one effect
- All method calls except the MUT should serve to initialize the test
- All assertions in the test should only verify the behavior of the single MUT
- A test may have multiple assertions, but only to determine the shape of a single output
- Verify state, not behavior
- Strongly prefer to
assertEquals()
on the result rather thanverify(mock).methodWasCalled()
assert
when you can,verify
when you must- Seeing both
assert
andverify
in a test is a smell–you should usually only need one or the other
- You may need multiple assertions to fully qualify the final state
- You rarely need more than one
verify
to qualify the behavior of the MUT - By only checking the state, which is the public contract, you allow the implementation to arrive at that state by various means (behaviors)
- Don't defeat this by
verify
ing the exact path through the MUT–doing so makes the test brittle without adding value
- Strongly prefer to
- Prefer real objects over mocks
- Strongly prefer to
assertEquals()
on the result rather thanverify(mock).methodWasCalled()
assert
when you can,verify
when you must- Seeing both
assert
andverify
in a test is a smell–you should usually only need one or the other
- You may need multiple assertions to fully qualify the final state
- You rarely need more than one
verify
to qualify the behavior of the MUT - By only checking the state, which is the public contract, you allow the implementation to arrive at that state by various means (behaviors)
- Don't defeat this by
verify
ing the exact path through the MUT–doing so makes the test brittle without adding value
- Strongly prefer to
- Prefer real objects over mocks
- If a class goes out of process (calls a service/database, touches the filesystem, etc.), use a mock
- If a class has non-deterministic behavior (calls
random()
, uses the clock, etc.), use a mock - If a class has complex construction requirements (you need to build a dozen objects to call the c'tor), use a mock
- Otherwise, use a real instance of the class
- Tests should drive the conversion of field/setter injection to c'tor injection
- C'tor injection is superior because it forces the developer to eliminate circular dependencies
- C'tor injection allows dependencies to be
final
- Setters provide more public interface which needs to be tested for full code coverage
- Field injection requires reflection to override in tests
- Tests must be fast
- The entire suite of unit tests should take no more than a few seconds to execute per library/module
- Fast tests facilitate a tight write-test-fix loop; slow tests discourage the execution of tests
- Do not let a test wait for some elapsed time to occur–the clock should be mocked in the first place, so it should be independently settable