Tests: Two for the price of one

promotional-special-offer-pre-printed-labels-2-for-1-oval-yellow-flexi-labels

Consider the following JUnit test:

private LocalDateTime now = LocalDateTime.now();
private LocalDateTime tomorrow = LocalDateTime.plus(1, DAYS);
private LocalDateTime yesterday = LocalDateTime.minus(1, DAYS);

@Test
public void dateComparisonWorksForPastAndFuture() {
    assertDateComparison(now, tomorrow, "is in the future");
    assertDateComparison(now, yesterday, "is in the past");
}

private void assertDateComparison(LocalDateTime baseline,
                                  LocalDateTime compare,
                                  String expected) {
    assertEquals(expected, MyDates.describe(baseline, compare);
}

It seems innocent enough, but there are two test smells in there.

Smell 1

The body of the test appears to be in a separate method. While it’s probably useful to extract some helpers to DRY out your tests, test methods need to be a little more explicit than to just delegate all of their work to another method. This other method doesn’t look too bad, but the larger the implementation in there, the more the actual test method seems like it’s just a puppet for the real test.

Smell 2

The single test method is really testing two independent use cases. It looks like that very obviously here, because I’ve positioned the set up of data in the body of the test class. You may find yourself making this mistake when you have quite a lot of set up to do within a method before you can start asserting, so you then bundle two separate use cases into the bottom of the method to make the most of the lengthy set up.

Either way, it’s a two-for-one deal, and that’s not good. Tests should be independent.

Is this so bad?

There’s no rule that says you need one assertion per unit test. Often, you can only prove the outcome of a test by checking multiple outcomes, or it’s useful to assert some lesser things before asserting the most important thing, so you can detect how close to passing the test is during development, or what caused it to fail during that bit of development that broke it.

However, one test should only cover one use case for pretty much the same reasons just mentioned. During development you should be focusing on one incremental use case at a time. At unit testing level, the use cases are at the micro scale or lower. One potential combination of inputs gets one unit test. You make that pass and then come up with the next one. Similarly, if you break something subtle, you really want one test to go bang and for that test to have a name which explains what you broke.

So what’s driving this?

When you have multiple assertions that essentially share the same code, you’re probably sitting on an example of a Parameterised test. For trivial examples of this the solution may be to extract some common setup and assertion code, and make each of the tests look like this:

@Test
public void useCase1() { // terrible name for a test!
   given(...); // common setup method in which we prepare

   Outcome outcome = when(...); // common method to do the thing under test

   // then assert plainly what you're expecting
   assertEquals("some expected thing", outcome.getTheThing());

The above allows us to refactor out the common setup/act bit of a test, while not obscuring the nature of the test.

But that sucks…

For >2 examples it may do, especially if the difference from one test to the other is just data; writing a half-dozen identical tests is dreadfully dull.

Parameterised tests are the answer.

Refactoring the original test might look like this:

// USING https://github.com/Pragmatists/JUnitParams
// with @RunWith(JUnitParamsRunner.class) on the test class

@Test
@Parameters({
    "1,is in the future",
    "-1,is in the past"
})
public void dateComparison(int dayOffset, String expected) {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime compare = LocalDateTime.plus(dayOffset, DAYS);

    assertEquals(expected, MyDates.describe(baseline, compare);
}

There are other ways to create parameterised tests, but this demonstrates the point. With JUnit’s own `Parameterized` runner, you need a whole class to parameterise a test. With the above runner you can mix parameterised and non parameterised tests in the same test class, which seems neater.

For more information about parameterised tests, please checkout my online tutorial at Udemy.

Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s