On Github ncapdevila / unit-testing-gems
Chris Hansen, Nicolas Capdevila
Designing unit tests is hard
It's so easy to duplicate/copy-paste code
Easy to cross boundaries (eg. mix persistence + business logic)
It's a burden. Leave it for later
2nd class citizen. Not as important as production code
"Nah, QA will catch this"
"Unit testing slows me down"
"This class is simple, not writing a test now"
Magic numbers
Erratic failures
Frequent Debugging
Slowness
Complex
Code duplication, repetition
Same standards as production
Readable. Code is read many more times than is written. Leave good impression behind
Sharp focus: "What am I testing?"
Strive for 100% coverage and meaningful assertions
Self documenting
Need care and love. Refactoring also applies
Fast
Design your test for extensibility
Use code coverage tools in your IDE and maven build
Use pair-programming or code reviews
TDD
Sonar
Have fun! Experiment without compromising the above... Dynamic language? Parallel execution?
Better sleeping patterns
Less bugs in production, more features/projects, better business
Earn respect from coworkers, users
Refactorings made easier
Pinpoint tech debt
Explore technologies
More employable. Big tech shops and luminaries get it!
It's fun
Use it for static-i stuff
instead of extending an overly complex BaseDbTestCase, BaseTestCase, ...
eg. CustomOlimpicTestRunner
public class CustomOlimpicTestRunner extends BlockJUnit4ClassRunner { Logger LOGGER = Logger.getLogger(CustomOlimpicTestRunner.class); public CustomOlimpicTestRunner(Class<!--?--> klass) throws InitializationError { super(klass); } @Override protected void runChild(FrameworkMethod method, RunNotifier notifier) { LOGGER.debug("About to run " + method.getName()); super.runChild(method, notifier); LOGGER.debug("After running " + method.getName()); } @Override protected Statement withBeforeClasses(Statement statement) { initializeConfig(); initializeDb(); return super.withBeforeClasses(statement); } @Override protected Statement withAfterClasses(Statement statement) { return super.withAfterClasses(statement); } private void initializeConfig() { // TODO Auto-generated method stub } private void initializeDb() { // TODO Auto-generated method stub } }
STOP repeating
1st draft: OlimpicsBusinessLogicTest
Improved: OlimpicsBusinessLogicImprovedTest
Better: ParameterizedOlimpicsBusinessLogicTest
@Test public void testWinGoldTightRace() { // GIVEN TrainingEffort me = HIGH; TrainingEffort[] competitors = new TrainingEffort[] { HIGH, HIGH, HIGH }; // WHEN Medal actual = logic.win(me, competitors); // THEN Medal expected = GOLD; assertEquals(expected, actual); } @Test public void testWinSilverTightRace() { // GIVEN TrainingEffort me = HIGH; TrainingEffort[] competitors = new TrainingEffort[] { HIGH, HIGH, IRONMAN }; // WHEN Medal actual = logic.win(me, competitors); // THEN Medal expected = SILVER; assertEquals(expected, actual); } /* STOP repeating yourself */ @Test public void test() { testIt(HIGH, new TrainingEffort[] { COUCH_POTATO, MEDIUM, MEH }, GOLD); testIt(HIGH, new TrainingEffort[] { HIGH, HIGH, HIGH }, GOLD); testIt(HIGH, new TrainingEffort[] { HIGH, HIGH, IRONMAN }, SILVER); testIt(MEDIUM, new TrainingEffort[] { HIGH, HIGH, MEH }, BRONZE); testIt(MEDIUM, new TrainingEffort[] { IRONMAN, HIGH, IRONMAN }, NONE_TRY_AGAIN_NEXT_TIME); } private void testIt(TrainingEffort me, TrainingEffort[] competitors, Medal expected) { // WHEN Medal actual = logic.win(me, competitors); // THEN assertEquals(expected, actual); } /* Can we do better? */ @Parameters public static Collection<Object[]> data() { Object[][] data = new Object[][] { { HIGH, new Object[] { COUCH_POTATO, MEDIUM, MEH }, GOLD } , { HIGH, new Object[] { HIGH, HIGH, HIGH }, GOLD } , { HIGH, new Object[] { HIGH, HIGH, IRONMAN }, SILVER } , { MEDIUM, new Object[] { HIGH, HIGH, MEH }, BRONZE } , { MEDIUM, new Object[] { IRONMAN, HIGH, IRONMAN }, NONE_TRY_AGAIN_NEXT_TIME } }; return Arrays.asList(data); } @Test public void test() { // WHEN Medal actual = logic.win(me, competitors); // THEN assertEquals(expected, actual); } public ParameterizedOlimpicsBusinessLogicTest( Object me, Object[] competitors, Object expected) { this.me = TrainingEffort.valueOf((String) me); this.competitors = competitors(competitors); this.expected = Medal.valueOf((String) expected); }
Empower your test: take action when your test fails or pass or runs. eg. create reports, alerts
Example: EventLoggerTest
@Rule public ReportCreator reportCreator = new ReportCreator(); @Rule public EventLoggerDestroyerRule rule = new EventLoggerDestroyerRule(); @Rule public ReportWriter writer = new ReportWriter(this, reportCreator.getFile()); @Before public void setUp() { eventLogger = new EventLogger.Impl(); } @Test(timeout = 150) public void testSave() throws Exception { Event event = new Event.Builder() .withCompetitionEvent(new CompetitionEvent(123, new URI("http://xterra.com"))) .withCompetitors(Lists.newArrayList( TrainingEffort.IRONMAN, TrainingEffort.MEDIUM, TrainingEffort.HIGH, TrainingEffort.COUCH_POTATO)) .withDate(new Date()) .withMedal(Medal.SILVER) .withMyCondition(TrainingEffort.HIGH) .build(); rule.setEvent(event); eventLogger.save(event); }
Let junit do the typing
@RunWith(Theories.class) public class IsMetalTest { private OlimpicsBusinessLogic.Impl logic; @Before public void setUp() { logic = new OlimpicsBusinessLogic.Impl(); } @DataPoints public static TrainingEffort[] me = new TrainingEffort[] { IRONMAN, HIGH, MEDIUM }; @DataPoints public static TrainingEffort[][] competitionICanBeat = new TrainingEffort[][] { { HIGH, HIGH, HIGH, HIGH } ,{ MEDIUM, HIGH, IRONMAN } ,{ IRONMAN, HIGH, IRONMAN } ,{ MEDIUM, HIGH, IRONMAN } }; @Theory public void testMetal(TrainingEffort me, TrainingEffort[] competitionICanBeat) { assumeTrue(me != MEDIUM); Medal actual = logic.win(me, competitionICanBeat); assertTrue(actual.isMetal()); }
Inspect side effects, eg. PerformanceTrackerTest
@Captor private ArgumentCaptor<Event> eventCaptor; //... @Test public void testBDD() throws Exception { //... verify(eventLogger).save(eventCaptor.capture()); Event actualEvent = eventCaptor.getValue(); assertEquals(medal, actualEvent.getMedal()); assertEquals(me, actual.getMyCondition()); assertEquals(competitionEvent, actual.getCompetitionEvent()); assertEquals(competitors, actual.getCompetitors()); assertEquals(medal, actual.getMedal()); } //... }
Introspect on the method calls of your mocks. eg. MapperTest
public class EventAnswer implements Answer<Event>{ //... @Override public Event answer(InvocationOnMock invocation) throws Throwable { Method method = invocation.getMethod(); if (method.getName().startsWith(GETTER_PREFIX)|| method.getName().startsWith(IS_PREFIX)) { invokedGetters.add(method.getName()); } return null /*don't care about returning anything, just that all getters are invoked*/; } //... public boolean isMissingGetInvocations() { return !getMissingGetInvocations().isEmpty(); } //... }
Mock parts of your unit under test. Use with caution.
public class SlowPerformanceTrackerSpyTest { //... @Before public void setUp() { //... underTest = spy(underTest); } //... @Test public void testWithSpy() { //... doReturn(event1).when(underTest).track(request1); doReturn(event2).when(underTest).track(request2); doReturn(event3).when(underTest).track(request3); // WHEN Collection<Event> actual = underTest.track(requests); // THEN assertEquals(3, actual.size()); }
public class ExampleTest { @Mock private List list; @Test public void shouldDoSomething() { list.add(100); } @Before public void setUp() { MockitoAnnotations.initMocks(this); } }
You decide!
@RunWith(MockitoJUnitRunner.class) public class ExampleTest { @Mock private List list; @Test public void shouldDoSomething() { list.add(100); } }
Set<Product> products = Sets.newHashSet(wallet, monitor); assertThat(products, hasSize(1));
java.lang.AssertionError: Expected: a collection with size <1> but: collection size was <2>
assertThat("Test content", allOf(startsWith("Test"), not(endsWith("content"))));
java.lang.AssertionError: Expected: (a string starting with "Test" and not a string ending with "content") but: not a string ending with "content" was "Test content"
Set<Product> products = Sets.newHashSet(wallet, monitor); assertThat(products, Matchers.<Product>hasItem( hasProperty("name", equalTo("Trombone"))));
java.lang.AssertionError: Expected: a collection containing hasProperty("name", "Trombone") but: property "name" was "Monitor", property "name" was "Wallet"
allOf(first, second, ...) anyOf(first, second, ...) both(...).and(...) either(...).or(...) equalTo(...) not(...) nullValue() anything() any(Class<...>) instanceOf(Class<...>) samePropertyValuesAs(...)
containsString(...) startsWith(...) endsWith(...) equalToIgnoringCase(...) equalToIgnoringWhiteSpace(...) isEmptyString() isEmptyOrNullString() hasToString(...)
greaterThan(...) greaterThanOrEqualTo(...) lessThan(...) lessThanOrEqualTo(...) closeTo(..., delta)
everyItem(...) hasItem(...) hasItems(...) hasSize(...)
Set<Product> products = Sets.newHashSet(wallet, monitor); assertThat(products).hasSize(1);
java.lang.AssertionError: Expected size:<1> but was:<2> in: <[Product{id=2, name='Wallet'}, Product{id=1, name='Monitor'}]>
assertThat("Test string").startsWith("Test").doesNotHave(suffix("string")); ... private Condition<? super String> suffix(final String suffix) { return new Condition<String>("suffix \"" + suffix + "\"") { @Override public boolean matches(String value) { return value.endsWith(suffix); } }; }
java.lang.AssertionError: Expecting: <"Test string"> not to have: <suffix "string">
Set<Product> products = Sets.newHashSet(wallet, monitor); assertThat(products).extracting("name").isEqualTo("Trombone");
org.junit.ComparisonFailure: Expected :"Trombone" Actual :["Monitor", "Wallet"]