Unit Testing Gems – About junit, mockito, assertj and more... much more – Hamcrest



Unit Testing Gems – About junit, mockito, assertj and more... much more – Hamcrest

1 0


unit-testing-gems

Code samples associated with the presentation about unit-testing

On Github ncapdevila / unit-testing-gems

Unit Testing Gems

About junit, mockito, assertj and more... much more

Chris Hansen, Nicolas Capdevila

Chris Hansen

  • Implemented automation-support internally (please don't throw things!)
  • Pair-programming proponent
  • Compulsive integration test writer

Nicolas Capdevila

  • Worked on large financial systems with over 80% code coverage
  • No pennies leakage!

Agenda

  • Realization
  • Smells
  • Guidelines
  • Benefits
  • JUnit gems
  • Mockito gems
  • Assertion gems
  • References
  • Q&A

Realization

  • Technical challenges
  • Cultural challenges

Realization

Technical challenges

Designing unit tests is hard

Realization

Technical challenges

It's so easy to duplicate/copy-paste code

Realization

Technical challenges

Easy to cross boundaries (eg. mix persistence + business logic)

Realization

Cultural challenges

It's a burden. Leave it for later

Realization

Cultural challenges

2nd class citizen. Not as important as production code

Realization

Cultural challenges

"Nah, QA will catch this"

Realization

Cultural challenges

"Unit testing slows me down"

Realization

Cultural challenges

"This class is simple, not writing a test now"

Smells

Magic numbers

Smells

Erratic failures

Smells

Frequent Debugging

Smells

Slowness

Smells

Complex

Smells

Code duplication, repetition

Guidelines

Same standards as production

Guidelines

Readable. Code is read many more times than is written. Leave good impression behind

Guidelines

Sharp focus: "What am I testing?"

Guidelines

Strive for 100% coverage and meaningful assertions

Guidelines

Self documenting

Guidelines

Need care and love. Refactoring also applies

Guidelines

Fast

Guidelines

Design your test for extensibility

Guidelines

Use code coverage tools in your IDE and maven build

Guidelines

Use pair-programming or code reviews

Guidelines

TDD

Guidelines

Sonar

Guidelines

Have fun! Experiment without compromising the above... Dynamic language? Parallel execution?

Benefits

Better sleeping patterns

Benefits

Less bugs in production, more features/projects, better business

Benefits

Earn respect from coworkers, users

Benefits

Refactorings made easier

Benefits

Pinpoint tech debt

Benefits

Explore technologies

Benefits

More employable. Big tech shops and luminaries get it!

Benefits

It's fun

JUnit gems

  • @RunWith and BlockJUnit4ClassRunner
  • @Parameterized
  • @Rule and Timeout
  • @RunWith(Theories.class), @Theory, @Datapoints, Assume

@RunWith and BlockJUnit4ClassRunner

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

  }
}

@Parameterized

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);
    }

@Rule and Timeout

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);
  }

@RunWith(Theories.class), @Theory, @Datapoints, Assume

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());
  }
  • This is a-must-read
  • @Captor
  • @Spy
  • BDD: Given-When-Then
  • Answer
  • MockitoJUnitRunner

@Captor

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());
  }
//...
}

Answer

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();
  }
//...
}

@Spy

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());
  }

Mockito boilerplate

public class ExampleTest {
    @Mock
    private List list;

    @Test
    public void shouldDoSomething() {
        list.add(100);
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }
}

MockitoJUnitRunner

You decide!

@RunWith(MockitoJUnitRunner.class)
 public class ExampleTest {
 
     @Mock
     private List list;
 
     @Test
     public void shouldDoSomething() {
         list.add(100);
     }
 }

Fluent assertions

  • A test is only as effective as its assertions.
  • assertTrue(boolean) and assertFalse(boolean) considered harmful!
    • Multiple conditions with && or || can be difficult to read
    • Expected true, but was: false

Hamcrest

  • A library of matcher objects.
    • Also known as constraints or predicates.
  • Not a testing library: it just happens that matchers are very useful for testing.

Hamcrest

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>
                    

Hamcrest

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"
                    

Hamcrest

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"
                    

Hamcrest

General matchers

allOf(first, second, ...)
anyOf(first, second, ...)
both(...).and(...)
either(...).or(...)
equalTo(...)
not(...)
nullValue()
anything()
any(Class<...>)
instanceOf(Class<...>)
samePropertyValuesAs(...)

Hamcrest

String matchers

containsString(...)
startsWith(...)
endsWith(...)
equalToIgnoringCase(...)
equalToIgnoringWhiteSpace(...)
isEmptyString()
isEmptyOrNullString()
hasToString(...)

Hamcrest

Numeric matchers

greaterThan(...)
greaterThanOrEqualTo(...)
lessThan(...)
lessThanOrEqualTo(...)
closeTo(..., delta)

Hamcrest

Collection / Iterable

everyItem(...)
hasItem(...)
hasItems(...)
hasSize(...)

Hamcrest

Pro tip

  • Eclipse

    Add org.hamcrest.Matchers to your “favorites” for completion of static imports.
  •  
  • IntelliJ IDEA

    Static method completion:
    • Ctrl Opt Space on Mac.
    • Ctrl Alt Space elsewhere.

AssertJ

  • Fluent assertions for Java.
  • A fork of FEST 2 (which never made it past milestone).

AssertJ

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'}]>
                    

AssertJ

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">
                    

AssertJ

Set<Product> products = Sets.newHashSet(wallet, monitor);
assertThat(products).extracting("name").isEqualTo("Trombone");
org.junit.ComparisonFailure:
Expected :"Trombone"
Actual   :["Monitor", "Wallet"]
                    

References

Thanks for your attention, Q & A