JUnit 5 tutorial, part 1: Unit testing with JUnit 5, Mockito, and Hamcrest

Set up your first Maven project and start writing robust unit tests with JUnit 5, Hamcrest, and Mockito

1 2 3 Page 3
Page 3 of 3

In addition to the groups property, JUnit 5 allows you to use an excludedGroups property to execute all tests that do not have the specified tag. For example, in a development environment, we do not want to execute the production tests, so we could execute the following:

mvn clean test -DexcludedGroups="Production"

This is helpful because in a large application you can have literally thousands of tests. If you wanted to create this environmental differentiation and add some new production tests, you would not want to have to go back and add a development tag to the other 10,000 tests.

Finally, you can add these same groups and excludedGroups fields to the surefire plug-in in your Maven POM file. You can also control these fields using Maven profiles. I encourage you to review the JUnit 5 User Guide to learn more about tags.

Introduction to Mock objects using Mockito

Thus far we have only reviewed testing simple methods that do not rely on external dependencies, but this is far from normal for large applications. For example, a business service probably relies on either a database or web service call to retrieve the data that it operates on. So how would we test a method in such a class? And how would we simulate problematic conditions, such as a database connection error or timeout?

The strategy of mock objects is to analyze the class under test and create mock versions of all of its dependencies, creating the scenarios that we want to test. You can do this manually—which is a lot of work—or you could leverage a tool like Mockito, which simplifies the creation and injection of mock objects into your classes. Mockito provides a simple API to create mock implementations of your dependent classes, inject the mocks into your classes, and control the behavior of the mocks.

The example in Listing 9 shows the source code for a simple repository.

Listing 9. Example repository (Repository.java)

package com.javaworld.geekcap.mockito;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;

public class Repository {
    public List<String> getStuff() throws SQLException {
        // Execute Query

        // Return results
        return Arrays.asList("One", "Two", "Three");
    }
}

Listing 10 shows the source code for a service that uses the above repository.

Listing 10. Example service (Service.java)


package com.javaworld.geekcap.mockito;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Service {
    private Repository repository;

    public Service(Repository repository) {
        this.repository = repository;
    }

    public List<String> getStuffWithLengthLessThanFive() {
        try {
            return repository.getStuff().stream()
                    .filter(stuff -> stuff.length() < 5)
                    .collect(Collectors.toList());
        } catch (SQLException e) {
            return Arrays.asList();
        }
    }
}

The Repository in Listing 9 has a single method, getStuff, that would presumably connect to a database, execute a query, and return the results. In this example, it simply returns a list of three Strings. The Service in Listing 10 receives the Repository through its constructor and defines a single method, getStuffWithLengthLessThanFive, which returns all Strings with a length less than 5. If the repository throws a SQLException then it returns an empty list.

Unit testing with JUnit 5 and Mockito

Now let's look at how we can test our service using JUnit 5 and Mockito. Listing 11 shows the source code for a ServiceTest class.

Listing 11. Testing the service (ServiceTest.java)

package com.javaworld.geekcap.mockito;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;

@ExtendWith(MockitoExtension.class)
class ServiceTest {
    @Mock
    Repository repository;

    @InjectMocks
    Service service;

    @Test
    void testSuccess() {
        // Setup mock scenario
        try {
            Mockito.when(repository.getStuff()).thenReturn(Arrays.asList("A", "B", "CDEFGHIJK", "12345", "1234"));
        } catch (SQLException e) {
            e.printStackTrace();
        }

        // Execute the service that uses the mocked repository
        List<String> stuff = service.getStuffWithLengthLessThanFive();

        // Validate the response
        Assertions.assertNotNull(stuff);
        Assertions.assertEquals(3, stuff.size());
    }

    @Test
    void testException() {
        // Setup mock scenario
        try {
            Mockito.when(repository.getStuff()).thenThrow(new SQLException("Connection Exception"));
        } catch (SQLException e) {
            e.printStackTrace();
        }

        // Execute the service that uses the mocked repository
        List<String> stuff = service.getStuffWithLengthLessThanFive();

        // Validate the response
        Assertions.assertNotNull(stuff);
        Assertions.assertEquals(0, stuff.size());
    }
}

The first thing to notice about this test class is that it is annotated with @ExtendWith(MockitoExtension.class). The @ExtendWith annotation is used to load a JUnit 5 extension. JUnit defines an extension API, which allows a third-party vendor like Mockito to hook into the lifecycle of running test classes and add additional functionality. The MockitoExtension looks at the test class, finds member variables annotated with the @Mock annotation, and creates a mock implementation of those variables. It then finds member variables annotated with the @InjectMocks annotation and attempts to inject its mocks into those classes, using either construction injection or setter injection.

In this example, MockitoExtension finds the @Mock annotation on the Repository member variable, so it creates a mock implementation of it and assigns it to the repository variable. When it sees the @InjectMocks annotation on the Service member variable, it creates an instance of the Service class, passing the mock Repository to its constructor. This allows us to control the behavior of the mock Repository class using Mockito's APIs.

In the testSuccess method, we use the Mockito API to return a specific result set when its getStuff method is called. The API works as follows: the Mockito::when defines the condition, which in this case is the invocation of the repository.getStuff() method. The when() method returns a org.mockito.stubbing.OngoingStubbing instance, which defines a set of methods that determine what to do when the specified method is called. In this case, we invoke the thenReturn() method to tell the stub to return a specific List of Strings.

At this point, we have a Service instance with a mock Repository. When the Repository's getStuff method is called, it returns a list of five known strings. We invoke the Service's getStuffWithLengthLessThanFive() method, which will invoke the Repository's getStuff() method, and return a filtered list of Strings whose length is less than five. We can then assert that the returned list is not null and that the size of it is three. This process allows us to test the logic in the specific Service method, with a known response from the Repository.

The testException method configures Mockito so that when the Repository's getStuff() method is called, it throws an SQLException. If this happens, the Service should not throw an exception; rather, it should return an empty list.

Mockito is a powerful tool and we've only scratched the surface of what it can do. If you've ever wondered how you can test abhorrent conditions—such as network, database, timeout, or other I/O error conditions—Mockito is the tool for you, and it works very elegantly with JUnit 5. If you do run into situations that Mockito does not support, such as mocking static member variables or private constructors, then there is another powerful but complex tool called PowerMock.

Conclusion

In this article, I've quickly introduced you to some of the highlights of working with JUnit 5. I showed you how to configure a Maven project to use JUnit 5 and how to write tests using the @Test and @ParameterizedTest annotations. I then introduced most of the JUnit 5 lifecycle annotations, reviewed the use and benefits of filter tags, and walked through integrating JUnit 5 with Hamcrest. Finally, I introduced Mockito and demonstrated how to use mock objects to test some of your more robust Java classes and scenarios.

We'll build on this foundation in the second half of this article, where you'll learn how to integrate JUnit 5 with the Spring framework. You'll learn how to use JUnit 5's built-in and third-party classes and extensions to test your Spring web controllers, services, and repositories.

This story, "JUnit 5 tutorial, part 1: Unit testing with JUnit 5, Mockito, and Hamcrest " was originally published by JavaWorld.

Copyright © 2020 IDG Communications, Inc.

1 2 3 Page 3
Page 3 of 3
How to choose a low-code development platform