Unit Testing With JUnit5

This is not an article about why you should do unit testing, rather this is about the how part. If you are writting in Java, probably its very synonymous to “writing JUnits”, this is because junit is one of the most popular libraries out there.

Let’s get started with a simple example

For Junit4 you would need the following dependency if you are doing maven.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

App.java

public class App {
    int add(int x, int y) {
        return x+y;
    }
}

AppTest.java

public class AppTest {
    private App app;

    @Before
    public void setup() {
         app = new App();
    }

    @Test
    public void testAdd() {
        Assert.assertEquals(5, app.add(2, 3));
    }
}

Exceptions

Its good to test the negative scenarios also i.e. if you are returning error in some condition , validate that as well.

For example if we want to avoid integer overflow or underflow

App.java

    int addSafely(int x, int y) {
        return Math.addExact(x, y);
    }

AppTest.java

    @Test(expected = ArithmeticException.class)
    public void testAddSafely() {
        app.addSafely(Integer.MAX_VALUE, 1);
    }

in junit 5

    @Test
    public void testAddSafely() {
        final Exception exception = assertThrows(ArithmeticException.class, () -> app.addSafely(Integer.MAX_VALUE, 1));
        assertEquals("integer overflow", exception.getMessage());
    }

In above example you are just testing one case. However when there is a complex logic in your main method, you may want to have test for several combinations (including exceptions cases)

    @ParameterizedTest
    @CsvSource(value = { "2, 3, 5", "1, 1, 2" }, delimiter = ',')
    public void testAdd(final int x, final int y, final int sum) {
        assertEquals(sum, app.add(x, y));
    }

The above test runs all the combinations mentioned in the @CsvSource annotation. However @ParameterizedTest is a Junit5 feature. This needs the following additional dependency.

    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-params</artifactId>
      <version>5.4.2</version>
      <scope>test</scope>
    </dependency>

Along with the following for the basic Junit5 tests

    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.5.2</version>
      <scope>test</scope>
    </dependency>

For junit4 there is only @RunWith(Parameterized.class) is available so the whole class is repeatably run instead of just a specific method.

Mocking

Not all methods are so simple like above which has just some input and output without any call other class/methods. If those other method and class are already having tests (in fact they should have their own test) we should not worry about them and assume they work properly while writing test for the caller method. This is why we need to “mock” them.

Let’s say we use a logger which is a core jdk feature and we know it would work, so we can mock it and just make sure it was called properly from our code because that’s where we can goof up.

App.java

     int add(int x, int y) {
        logger.info("Adding numbers");
        return x + y;
    }

AppTest.java

 @ExtendWith(MockitoExtension.class)
 public class AppTest {

    private App app;
    
    @Mock
    private Logger logger;

    @BeforeEach
    public void setup() {
        doNothing().when(logger).info(any(String.class));
        app = new App(logger);
    }
    
    public void testAdd() {
        assertEquals(5, app.add(2, 3));
        verify(logger, times(1)).info("Adding numbers");
    }

If you don’t have exact match to be done, there are several other matches available e.g. If you have the following log statement logger.info("Result is "+x); You need

org.mockito.ArgumentMatchersstartsWith("Result is");

Asserting objects Assert scenarios are not always simple literal match, it could be a complex object in which you want to match some attributes. ReflectionMatcher comes handy in this case.

Assert.assertTrue(new ReflectionEquals(expected, excludeFields).matches(actual));