Unit Tests
The Good, the Bad and the Ugly
Michał Urbanek
What is it not about?
- Integration testing
- UI testing
- Acceptance testing
- Smoke and sanity testing
- Security testing
- Stress testing
- ...
What is it about then?
Unit Tests


Benefits of having UT
- find problems early
- facilitates change
- documentation
- design

Good Unit Tests
- trustworthy
- maintainable
- readable
Trustworthy
- test the right thing
- make it easy to run
- run them often
- failure == problem
Piece of cake?
It is a full time job!

Obvious, right?
- Always see the failing test first
- Keep your tests independent from each other - enforce test isolation
- Clean the environment before tests, not afterwards
- DRY = reuse test code
Naming
public void testCreateUser()
public void testCreateUser2()
public void createUser_populated_OK()
public void shouldCreateUserStoreUserInDatastoreAndPersistAllFields()WRONG
Start with should.
Think about the scenario.
Do not use the test prefix.
Structure
public void shouldCreateUserStoreUserInDatastoreAndPersistAllFields() {
// given
User user = new User(id)
// when
user.save()
// then
assertThat(User.get(id)).isEqualTo(user)
}KISS
- No logic in tests! Even the simplest will be evil!
- test logic == test bugs
- No ifs or switches
-
Only
- create/configure
- act
- assert
What to test?
- Write tests which make you confident that your system works
- Test everything that can possibly break!
- When your code evolves, take care that your
tests also evolve

- Single Responsibility Principle (avoid multiple asserts)
"Each test method should verify just one scenario".
- Test only public methods
"Separate the business scenario from low-level details" - Or even better
"Test Behaviour Not Methods!"
What to test?
The rules are there
to be broken
@Test
public void shouldRecognizeDistrict() {
//given
..
//when
City city = new City(district, NUMBER_OF_PEOPLE);
//then
assertThat(city.isLocatedIn(district)).isTrue();
assertThat(city.isLocatedIn(anotherDistrict)).isFalse();
}@Test
public void shouldRecognizeItsDistrict() {
//given
..
//when
City city = new City(district, NUMBER_OF_PEOPLE);
//then
assertThat(city.isLocatedIn(district)).isTrue();
}
@Test
public void shouldRecognizeDifferentDistrict() {
//given
..
//when
City city = new City(district, NUMBER_OF_PEOPLE);
//then
assertThat(city.isLocatedIn(anotherDistrict)).isFalse();
}Assertions
- standard JUnit
- Hamcrest
- FEST assertions/AssertJ
AssertJ
// unique entry point to get access to all assertThat methods and utility methods
import static org.assertj.core.api.Assertions.*;- rich and easy to use
- customizable
- support for guava/joda
AssertJ
// common assertions
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron)
.isIn(fellowshipOfTheRing);
assertThat(sauron).isNotIn(fellowshipOfTheRing);// String specific assertions
assertThat(frodo.getName()).startsWith("Fro")
.endsWith("do")
.isEqualToIgnoringCase("frodo");AssertJ
// collection specific assertions
assertThat(fellowshipOfTheRing).hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);
// extract properties
assertThat(fellowshipOfTheRing).extracting("name").contains("Boromir", "Gandalf", "Frodo")
.doesNotContain("Sauron", "Elrond");
assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name")
.contains(tuple("Boromir", 37, "Man"),
tuple("Sam", 38, "Hobbit"),
tuple("Legolas", 1000, "Elf"));
// map specific assertions
assertThat(ringBearers).hasSize(4)
.contains(entry(oneRing, frodo), entry(nenya, galadriel))
.containsEntry(narya, gandalf)
.doesNotContainEntry(oneRing, aragorn);AssertJ
- Assertions on results of method call on iterable
- Collect all errors with Soft assertions
- Using String assertions on the content of a file
- Filtering a group of objects before making assertions
- Reflection based assertions
Readable assertions
@Test
public void testChargeInRetryingState() throws Exception {
// given
TxDTO request = createTxDTO(RequestType.CHARGE);
AndroidTx androidTx = ...
Processor processor = ...
... much more complex set-up code here
// when
final TxDTO txDTO = processor.processRequest(request);
// then
assertEquals(txDTO.getResultCode(), ResultCode.SUCCESS);
final List<AndroidTxStep> steps = new ArrayList<AndroidTxStep>(
androidTx.getTxSteps());
final AndroidTxStep lastStep = steps.get(steps.size() - 1);
assertEquals(lastStep.getTxState(),
AndroidTxState.CHARGE_PENDING);
assertEquals(lastStep.getMessage(), ClientMessage.SUCCESS);
... some more assertions here
}@Test
public void testChargeInRetryingState() throws Exception {
// given
TxDTO request = createTxDTO(RequestType.CHARGE);
AndroidTx androidTx = ...
Processor processor = ...
... much more complex set-up code here
// when
final TxDTO txDTO = processor.processRequest(request);
// then
assertState(ResultCode.SUCCESS, androidTx,
AndroidTxState.CHARGE_PENDING, ClientMessage.SUCCESS;
}@Test
public void testChargeInRetryingState() throws Exception {
// given
TxDTO request = createTxDTO(RequestType.CHARGE);
AndroidTx androidTx = ...
Processor processor = ...
... much more complex set-up code here
// when
final TxDTO txDTO = processor.processRequest(request);
// then
assertEquals(txDTO.getResultCode(), ResultCode.SUCCESS);
assertThat(androidTx)
.hasState(AndroidTxtate.CHARGED)
.hasMessage(ClientMessage.SUCCESS)
.hasPreviousState(AndroidTxState.CHARGE_PENDING)
.hasExtendedState(null);
}Readable assertions
@Test
public void invalidTxShouldBeCanceled() {
... some complex test here
// then
String fileContent =
FileUtils.getContentOfFile("response.csv");
assertTrue(fileContent.contains("CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,"));
}@Test
public void invalidTxShouldBeCanceled() {
... some complex test here
// then
String fileContent =
FileUtils.getContentOfFile("response.csv");
TxDTOAssert.assertThat(fileContent).hasTransaction("123cancel").withResultCode(SUCCESS);
}VS
Not enough tests
public class FizzBuzzTest {
@Test
public void testMultipleOfThreeAndFivePrintsFizzBuzz() {
assertEquals("FizzBuzz", FizzBuzz.getResult(15));
}
@Test
public void testMultipleOfThreeOnlyPrintsFizz() {
assertEquals("Fizz", FizzBuzz.getResult(93));
}
@Test
public void testMultipleOfFiveOnlyPrintsBuzz() {
assertEquals("Buzz", FizzBuzz.getResult(10));
}
@Test
public void testInputOfEightPrintsTheNumber() {
assertEquals("8", FizzBuzz.getResult(8));
}
}@RunWith(JUnitParamsRunner.class)
public class FizzBuzzJUnitTest {
@Test
@Parameters(value = {"15", "30", "75"})
public void testMultipleOfThreeAndFivePrintsFizzBuzz(int multipleOf3And5) {
assertEquals("FizzBuzz", FizzBuzz.getResult(multipleOf3And5));
}
@Test
@Parameters(value = {"9", "36", "81"})
public void testMultipleOfThreeOnlyPrintsFizz(int multipleOf3) {
assertEquals("Fizz", FizzBuzz.getResult(multipleOf3));
}
@Test
@Parameters(value = {"10", "55", "100"})
public void testMultipleOfFiveOnlyPrintsBuzz(int multipleOf5) {
assertEquals("Buzz", FizzBuzz.getResult(multipleOf5));
}
@Test
@Parameters(value = {"2", "16", "23", "47", "52", "56", "67", "68", "98"})
public void testInputOfEightPrintsTheNumber(int expectedNumber) {
assertEquals("" + expectedNumber, FizzBuzz.getResult(expectedNumber));
}
}Name variables
Avoid magic numbers/strings
(use constants with meaningful names)
@DataProvider
public static Object[][] userPermissions() {
return new Object[][]{
{"user_1", READ},
{"user_2", READ},
{"user_2", WRITE},
{"user_3", READ},
{"user_3", WRITE},
{"user_3", DELETE}
};
}@DataProvider
public static Object[][] userPermissions() {
return new Object[][]{
{"guest", READ},
{"logged", READ},
{"logged", WRITE},
{"admin", READ},
{"admin", WRITE},
{"admin", DELETE}
};
}Expect exception anywhere
@Test(expected=IndexOutOfBoundsException.class)
public void testNegative() {
MyList<Integer> list = new MyList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(3);
list.add(4);
list.get(-1);
}@Test
public void shouldThrowExceptionWhenTryingToGetElementOutsideTheList() {
// given
MyList<Integer> list = new MyList<Integer>();
list.add(0);
list.add(1);
list.add(2);
// when
catchException(list).get(3);
// then
assertThat(caughtException())
.isExactlyInstanceOf(IndexOutOfBoundsException.class);
}Creation of objects
@Before
public void initialize() {
User user = new User("email@example.com", "Example", "Example", "qwerty",
"Europe/Warsaw", UserState.NOT_VERIFIED, new Address());
...
}
@Test
public void shouldCommitTransaction() {
User user = new User("firstName", "lastName", "password",
"email@example.com", "qwerty", UserState.ACTIVE, new Address());
user.setRegistrationDate(oneDayAgo.toDate());
user.setAccessCode("qwerty");
...
}
@Test
public void shouldGetUserByCompanyData() {
User user = new User("email", "FirstName", "LastName", "Password",
"Europe/Warsaw", UserState.ACTIVE, address);
user.setRegistrationDate(new Date());
user.setCompany(company);
user.setAccessCode("Access Code");
...
}@Before
public void initialize() {
User notVerifiedUser = UserBuilder.createUser(UserState.NOT_VERIFIED)
.create();
...
}
@Test
public void shouldCommitTransaction() {
User user = UserBuilder.createActiveUser()
.create();
...
}
@Test
public void shouldGetUserByCompanyData() {
User user = UserBuilder.createActiveUser()
.withCompany(company)
...
}MockServer server = new MockServer(responseMap, true,
new URL(SERVER_ROOT).getPort(), false);Creation of objects
private static final boolean RESPONSE_IS_A_FILE = true;
private static final boolean NO_SSL = false;
MockServer server = new MockServer(responseMap, RESPONSE_IS_A_FILE,
new URL(SERVER_ROOT).getPort(), NO_SSL);private MockServer noSslFileServer() throws MalformedURLException {
return new MockServer(responseMap, true,
new URL(SERVER_ROOT).getPort(), false);
}
MockServer server = noSslFileServer();MockServer server = new MockServerBuilder()
.withResponse(responseMap)
.withResponseType(FILE)
.withUrl(SERVER_ROOT)
.withoutSsl()
.create();
MockServer server = new MockServerBuilder()
.createFileServer(SERVER_ROOT)
.withResponse(responseMap)
.create();@Test
public void shouldAddTimeZoneToModelAndView() {
//given
Context context = mock(Context.class);
ModelAndView modelAndView = mock(ModelAndView.class);
given(context.getTimezone()).willReturn("timezone X");
//when
new UserDataInterceptor(context)
.postHandle(null, null, null, modelAndView);
//then
verify(modelAndView).addObject("timezone", "timezone X");
}
// argument matchers
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// verify number of interactions
verify(mockedList, times(2)).add("twice");
verify(mockedList, never()).add("never happened");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");
// stub method to throw an exception
doThrow(new RuntimeException()).when(mockedList).clear();
// verify in order
List singleMock = mock(List.class);
InOrder inOrder = inOrder(singleMock);
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
//verify no interaction
verifyZeroInteractions(mockedObject);

import static org.mockito.BDDMockito.*;
Seller seller = mock(Seller.class);
Shop shop = new Shop(seller);
public void shouldBuyBread() throws Exception {
//given
given(seller.askForBread()).willReturn(new Bread());
//when
Goods goods = shop.buyBread();
//then
assertThat(goods, containBread());
}

Mocks Are Good
@Test
public void shouldGetTrafficTrend() {
//given
TrafficTrendProvider trafficTrendProvider
= mock(TrafficTrendProvider.class);
Report report = new Report(null, "", 1, 2, 3,
BigDecimal.ONE, BigDecimal.ONE, 1);
TrafficTrend trafficTrend = new TrafficTrend(report, report,
new Date(), new Date(), new Date(), new Date());
given(trafficTrendProvider.getTrafficTrend()).willReturn(trafficTrend);
TrafficService service = new TrafficService(trafficTrendProvider);
//when
TrafficTrend result = service.getTrafficTrend();
//then
assertThat(result).isEqualTo(trafficTrend);
}@Test
public void shouldGetTrafficTrend() {
//given
TrafficTrendProvider trafficTrendProvider
= mock(TrafficTrendProvider.class);
TrafficTrend trafficTrend = mock(TrafficTrend.class);
given(trafficTrendProvider.getTrafficTrend()).willReturn(trafficTrend);
TrafficService service = new TrafficService(trafficTrendProvider);
//when
TrafficTrend result = service.getTrafficTrend();
//then
assertThat(result).isEqualTo(trafficTrend);
}Creating objects only to create other objects, so you can create other objects?
Do not do that!
(Unless the creation of objects is what you want to test).
Jersey Test Framework
public class SimpleTest extends JerseyTest {
@Path("hello")
public static class HelloResource {
@GET
public String getHello() {
return "Hello World!";
}
}
@Override
protected Application configure() {
return new ResourceConfig(HelloResource.class);
}
@Test
public void test() {
final String hello = target("hello").request().get(String.class);
assertEquals("Hello World!", hello);
}
}grizzly vs in memory
Jersey Test Framework
@RunWith(MockitoJUnitRunner.class)
public class TrackerFacadeTest extends BaseFacadeTest {
@Override
protected Object facadeInstance() {
return trackerFacade;
}
@Test
public void shouldCallBee7ServiceAndReturnNoContent() {
// given
MultivaluedMap<String, String> params = new Bee7ConversionParamsBuilder() //
.advertisingId("adid") //
.app(App.ANDROID_MY_TALKING_TOM) //
.countryCode("CN") //
.platform(Platform.ANDROID) //
.timestamp(123L) //
.build();
// when
ClientResponse response = resource()
.path(CONVERSION_PATH)
.queryParams(params)
.get(ClientResponse.class);
// then
assertThat(response.getStatus(), is(Response.Status.NO_CONTENT.getStatusCode()));
// verify that bee7Service was really called
}
}WebStub
private void mockJerseyClient() {
Client client = mock( Client.class );
WebResource webResource = mock( WebResource.class );
WebResource.Builder builder = mock( WebResource.Builder.class );
ClientResponse clientResponse = mock( ClientResponse.class );
when( builder.get( ClientResponse.class ) ).thenReturn( clientResponse );
when( clientResponse.getEntity( String.class ) ).thenReturn( "true" );
when( webResource.accept( anyString() ) ).thenReturn( builder );
when( client.resource( anyString() ) ).thenReturn( webResource );
}@BeforeClass
public static void beforeAll() {
server = newServer(9099);
stubServer = server.withContext("/context");
server.start();
}
@Before
public void setUp() {
stubServer.reset();
}
@Test
public void shouldStubHttpCalls() {
stubServer.get("/accounts/1").returns(response(200).withContent("account details"));
Response response = httpClient.get("http://localhost:9099/context/accounts/1");
assertThat(response.status(), is(200));
assertThat(response.content(), is("account details"));
}
@AfterClass
public static void afterAll() {
server.stop();
}@Test
public void testWithAtomicPseudoClosure() {
await()
.atMost(10, SECONDS)
.untilCall(
to(userRepository).size(), equalTo(3));
}
private AtomicInteger atomic = new AtomicInteger(0);
@Test
public void testWithAtomicNumber() {
await()
.untilAtomic(atomic, equalTo(1));
}Awaitility
Doing things the right way takes more time than doing them any old way.
But this extra effort usually pays off in the long term.
Things to Remember
Hard to write a test?
Maybe the production code is of low quality?
...or maybe you should consider writing tests before production code?
Things to Remember
Things to Remember
Be pragmatic, and let experience be your guide.
It doesn’t matter if someone advises you to employ some technique or other.
If it doesn’t work for you, simply don’t do it!
References
- http://joel-costigliola.github.io/assertj/
- http://code.google.com/p/catch-exception/
- https://code.google.com/p/junitparams/
- https://code.google.com/p/mockito/
- https://code.google.com/p/awaitility/
- https://github.com/tusharm/WebStub
- Lasse Koskela "Effective Unit Testing"
- Tomasz Kaczanowski "Practical Unit Testing"
- Tomasz Kaczanowski "Bad Tests, Good Tests"


Unit Tests - The Good, the Bad and the Ugly
By urbmic
Unit Tests - The Good, the Bad and the Ugly
- 1,070