Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Dealing With Similar Tests in JUnit 5

DZone's Guide to

Dealing With Similar Tests in JUnit 5

Similar tests could be quite an issue in JUnit 4. Fortunately, the new version brings alternatives in the form of grouping assertions and dynamic test cases.

· Java Zone
Free Resource

Try Okta to add social login, MFA, and OpenID Connect support to your Java app in minutes. Create a free developer account today and never build auth again.

From time to time when writing unit tests, we notice that a bunch of our tests are very similar — and that this similarity could use some extra care. In JUnit 4, there weren't too many ways to handle such cases in a nice manner. Let's see what JUnit 5 has for us in this area.

Example

Imagine you're working with an IQ-related application and you want to represent the newest Stanford-Binet Intelligence scale. Let's say that you played a bit and the implementation that fits your application looks like this:

import java.util.stream.Stream;

public enum StanfordBinetIntelligence {
    VERY_GIFTED(145, 160),
    GIFTED(130, 144),
    SUPERIOR(120, 129),
    HIGH_AVERAGE(110, 119),
    AVERAGE(90, 109),
    LOW_AVERAGE(80, 89),
    BORDERLINE_IMPAIRED(70, 79),
    MILDLY_IMPAIRED(55, 69),
    MODERATELY_IMPAIRED(40, 54);

    private final int lowerRange;
    private final int upperRange;

    StanfordBinetIntelligence(int lowerRange, int upperRange) {
        this.lowerRange = lowerRange;
        this.upperRange = upperRange;
    }

    public static StanfordBinetIntelligence forIQ(int iq) {
        return Stream.of(values())
                .filter(it -> it.withinRange(iq))
                .findAny()
                .orElseThrow(IllegalArgumentException::new);
    }

    private boolean withinRange(int iq) {
        return (iq >= lowerRange) && (iq <= upperRange);
    }
}


What you want to do now is thoroughly test the forIQ method. For the sake of having a workable example, let's assume that the design is fine and we just want to test it inside out.

One by One

Of course, testing things one by one is not the best option. We'd end up with a ton of similar tests:

public class StanfordBinetIntelligenceTest {

    @Test
    public void forIQshouldReturnVeryGiftedFor145IQ() throws Exception {
        assertEquals(VERY_GIFTED, forIQ(145));
    }

    @Test
    public void forIQshouldReturnVeryGiftedFor150IQ() throws Exception {
        assertEquals(VERY_GIFTED, forIQ(150));
    }

    @Test
    public void forIQshouldReturnVeryGiftedFor160IQ() throws Exception {
        assertEquals(VERY_GIFTED, forIQ(160));
    }

    // etc.
}

All in One

We could, of course, give up on separating each test case into a method and just bash all of the assertions in a single one. The downsides of this approach would be having everything treated like a single test and the test blowing off at the first failed assertion. If there was more than one assertion failure, we wouldn't know about it until we fix the first issue and run the tests again.

Leveraging assertAll

The first new alternative that would help us get the number of tests a little smaller is grouping the tests by level and the assertions with newly introduced assertAll construct. With this, we can be sure that all of the assertions will be fired and all of the failures logged.

public class StanfordBinetIntelligenceTest {

    @Test
    public void forIQshouldReturnVeryGiftedWithinItsRange() throws Exception {
        assertAll(
                () -> assertEquals(VERY_GIFTED, forIQ(145)),
                () -> assertEquals(VERY_GIFTED, forIQ(150)),
                () -> assertEquals(VERY_GIFTED, forIQ(160))
        );
    }

    // etc.
}


Of course, we could go even further and bring the assertions of all IQ levels in a single test method. We'll skip the example for brevity.

This assertAll approach works well, but all the assertions are still considered a part of a single test. Depending on your taste and coding style, this might be a good or a bad thing.

Leveraging Dynamic Tests

Here comes the second option introduced in JUnit 5 — dynamic tests. By changing the @Test annotation to @TestFactory and returning a collection of DynamicTest objects, we can make the IDE treat each assertion as a separate test:

public class StanfordBinetIntelligenceTest {

    @TestFactory
    public Collection<DynamicTest> veryGiftedTests() throws Exception {
        return Arrays.asList(
                dynamicTest("Very Gifted for 145 IQ", () -> assertEquals(VERY_GIFTED, forIQ(145))),
                dynamicTest("Very Gifted for 150 IQ", () -> assertEquals(VERY_GIFTED, forIQ(150))),
                dynamicTest("Very Gifted for 160 IQ", () -> assertEquals(VERY_GIFTED, forIQ(160)))
        );
    }

    // etc.
}


As you can see, the IDE handles it nicely:

Image title

Mixing the Two?

One could think about going even further and mixing the two approaches, i.e. use assertAll to group assertions for a given intelligence level and create a dynamic test for each of the levels. BUT!

  1. That would be logically equivalent to the assertAll option with grouping regular tests by levels.

  2. That would be really complex, and you don't want to live with that complexity.

Final Thoughts

As you can see, with the new JUnit version, we have more flexibility when it comes to tedious test that would bloat our codebase, be hard to debug, or require special runners. Whichever you choose is a matter of case and taste. Personally, I'd stick to Spock and it's nice data tables.

Build and launch faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
junit 5 ,java ,unit testing ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}