Micro Focus is now part of OpenText. Learn more >

You are here

You are here

How to extend Java unit tests to components with Spring, Mockito

public://pictures/Alon Har-El.jpg
Alon Har-El Senior Software Engineer, HPE
 

In continuous integration/continuous delivery (CI/CD) environments, where release times are short and frequent, it's hard to ensure that changes in the code don't cause regressions and introduce new defects. In these situations, manually testing changes becomes a massive task, so your team will need to automate tests as much as possible.

When it comes to Java unit testing specifically, there are two major challenges: 1. How do you define a unit? Should it be defined as a class, or as a specific logical flow? 2. Which tools/solutions should you use for this type of testing?

Here are two approaches for Java unit testing. The first considers the class as the unit and uses the mocking framework Mockito. The next considers the logical flow as the unit, hence the test involves several classes working together. For the second approach, we'll use the combination of Mockito and the Spring framework.

It's important to understand each approach, how to use these tools to mark classes for testing, and how to string together classes for testing. Here are two examples based on our team’s experience: one that uses Mockito alone to test single classes, and another that uses the Spring framework to replace real class implementations with Mockito mocks, making it possible to test entire flows.

Unit tests versus component/integration tests

Two test types are commonly used when testing Java code:

  • Unit tests break the application into its fundamental elements. We test each element separately from the other so that the focus is more on correctness and quality. In Java, we usually refer to the "class" as the object under test.

  • Component tests will test the system without other third-party code and services that are beyond the team’s responsibility. The focus of these tests is more on functionality, where we assume that the code tested at the unit level is performing as expected.

Unit tests written with Mockito can be extended to component tests by using the Spring framework.

Example application and flow

For an example of a library management application that manages a library’s books and readers (add book, add reader, remove reader, etc.), we will focus on the following application elements:

  • Library: Library service that provides methods for managing the library

  • Library Data Access Layer (DAL)

  • Readers service: Responsible for handling the library’s readers

  • Notification service: Responsible for sending notifications

Among other things, our library application enables the librarian to add a new book to the library. When a new book is added, two things happen:

  • The new book is stored in the library database.

  • Readers who are subscribed will receive some type of notification (email, SMS, etc.) about the new book that is now available in the library.

Our tests focus on this flow, and we use two testing methodologies. The first will be a "classical" form of unit testing, where we test each element in our application separately from the other. The second testws the entire flow, and therefore will need to include several if not all of the application’s elements.

First approach: Testing the class with Mockito alone

Mockito is a common mocking framework for unit tests written in Java. The framework allows the creation of mock objects in automated unit tests for the purpose of test-driven development (TDD). These mock testing frameworks effectively "fake" some external dependencies, so that the object being tested has a consistent interaction with its outside dependencies. Mockito works to streamline the delivery of these external dependencies that are incidental to the test. As a result, Mockito helps you build maintainable, high-quality tests that enable continuous delivery.

When you narrow down the "unit" to be the basic object-oriented element—i.e., the class—you will probably use Mockito alone. You will mock all of the class’s dependencies, leaving just the class’s logic to be tested.

Unit (class) test

With unit testing, we isolate each element in the application from the others. As mentioned previously, when a new book is added to the library, we store it to the database and send notifications to the readers. The implementation of the newBook method in the Library class uses both the library DAL and the readers service, as follows:

public void newBook(String bookName, String author)
{

// Add the new book to the database:
String bookId = libraryDal.storeBook(bookName, author);

// Inform readers about the new book:
readersService.newBookAdded(bookId, bookName, author);
...
}

Our test doesn’t care what the library DAL or readers service does and whether they do it correctly. It just needs to verify that, given a specific book name and a specific author, the appropriate database insert is made and the readers service is correctly informed.

To achieve this goal, we need to use mocks that fake the real library DAL and readers service implementation. With the appropriate Mockito annotations, Mockito knows to scan the class under test and replace the class dependencies—the class members—with proxies that respond in the way that the test’s writer wants them to respond. Mockito "injects" mocks into the tested class.

The following code illustrates this point:

@InjectMocks
private LibraryImpl library;
@Mock
private LibraryDal libraryDal;
@Mock
private ReadersService readersService;
@Before
public void init()
{
// Inject mocks to the library class:
MockitoAnnotations.initMocks(this);
// Return book id upon book name and author:
doAnswer(new Answer()
{
    @Override
    public Object answer(InvocationOnMock invocationOnMock) throws Throwable
    {
        Object[] arguments = invocationOnMock.getArguments();
        return "BookId_" + arguments[0].toString() + "_" + arguments[1].toString();
    }
}).when(libraryDal).storeBook(anyString(), anyString());
}

Now we can test to see if our library class is performing and invoking the other elements (DAL and readers service), as expected:

@Test
public void testNotification()
{
// Test the new book method:
library.newBook("The Trial", "Kafka");
// Verify we stored the book in the database and called the readers service:
verify(libraryDal, times(1)).storeBook("The Trial", "Kafka");
verify(readersService, times(1)).newBookAdded("BookId_The Trial_Kafka", "The Trial", "Kafka");
}

We can use the same mechanism to test other application components. We will mock their dependencies and verify that, given a specific input, they perform a specific flow and generate a specific output.

Second approach: Testing the flow with Spring

When testing a flow with Spring, you consider a specific business logic flow (i.e., user story) to be the unit, meaning that several classes from different layers that are responsible for a specific flow might be tested together. However, an object under test might have dependencies on other objects. It might need to interact with a database, communicate with a mail server, or talk to a web service or a message queue. All these services might not be available during unit testing and therefore should be mocked.

One of Spring’s central features is its inversion of control (IoC) design, which enables loading class implementations at runtime. Since the implementation is loaded at runtime, it’s possible to choose a mock implementation that fits the test.

On the one hand, you cannot mock all the class dependencies, because they might be part of the flow that you want to test. On the other hand, you still want to modify their behavior to adjust them to your specific tested flow. The combination of Spring and Mockito allows you to construct mock objects using Spring. Spring will inject your mocks into your flow at any layer and any point, so you can test a well-defined flow.

Flow test

This tests the entire flow of adding a new book to the library and the relevant users receiving notifications.

The flow is as follows:

  1. Store the new book in the database

  2. Inform the readers service about the new book

  3. The readers service will:

    1. Get a list of all readers

    2. Construct a sub-list (from the readers list) that contains only the users who asked to be notified

    3. Send a notification to all users who asked to be notified

The following diagram illustrates the flow:

In the above diagram, objects and methods in red represent mock objects and mock calls that imitate database interactions. The notification service is mocked too. We don’t want an actual notification, and our flow ends when we call it.

The main flow, however, is not mocked; the new book in the library results in notifications for all interested readers. Our application uses Spring’s mechanism to inject the implementations. In order to inject the implementation, we need to notify Spring about:

  1. Class members that need Spring to construct their concrete implementations. In this example, we will use Spring annotation @Autowired to indicate these members.

  2. A container that holds the concrete implementations. In this example, we will use a Java configuration file for this job.

The combination of annotating class members that will be constructed by Spring, and a configuration file that indicates which implementation to construct, is where we intervene and inject the Mockito mock implementation that supports our testing.

To inject the Mockito mocks, our test will run with Spring, and we need to tell Spring where to look for its beans:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = LibraryTestConfiguration.class)
public class LibraryTestSpring 

In the LibraryTestConfiguration class, we define the Spring beans that are Mockito mocks:

@Configuration
public class LibraryTestConfiguration
{
  @Bean
  public Library library()
  {
      return new LibraryImpl();
  }
  @Bean
  public LibraryDal libraryDal()
  {
      return mock(LibraryDal.class);
  }
  @Bean
  public ReadersService reader()
  {
      return spy(new ReadersServiceImpl());
  }
  @Bean
  public NotificationService notificationService()
  {
      return mock(NotificationService.class);
  }
}

Notes:

  1. The library service is a real object.

  2. The database layer is entirely mocked with Mockito.

  3. We use Mockito to spy on ReadersService. We want to use it as-is, but still mock the methods that use the database.

  4. The endpoint notification service is a mock. Our flow ends when we verify that it is called correctly.

The Spring/Mockito test

Our joint Spring/Mockito test verifys the following flow.

Assume that our library has two readers, Alice and Bob, and that only Alice is interested in getting notifications from the library. When we add a new book to the library, The Trial, by Franz Kafka, Alice is notified about the new book and Bob is not.

We start the test by auto-wiring all flow participants:

@Autowired
private Library library;
@Autowired
private LibraryDal libraryDal;
@Autowired
private NotificationService notificationService;
@Autowired
private ReadersService readersService; 

Spring will inject the objects we defined in our Spring configuration file—Mockito mocks and spies. The next step is to prepare the preliminary flow conditions:

@Before
public void init()
{
// All library's readers:
Collection<ReaderBean> readers = new HashSet<>();
readers.add(new ReaderBean("Alice", "alice@alice.com", true/*needToNotify*/));
readers.add(new ReaderBean("Bob", "bob@bob.com", false/*needToNotify*/));
doReturn(readers).when(readersService).getAllReaders();
// Return book id upon the book name and author
doAnswer(new Answer()
{
    @Override
    public Object answer(InvocationOnMock invocationOnMock) throws Throwable
    {
        Object[] arguments = invocationOnMock.getArguments(); 
        return "BookId_" + arguments[0].toString() + "_" + arguments[1].toString();
    }
}).when(libraryDal).storeBook(anyString(), anyString());
}

Now we can perform the test:

// Start new book flow:
library.newBook("The Trial", "Kafka");
// Verify we sent a notification only to Alice but not to Bob:
verify(notificationService, times(1)).sendNotification("alice@alice.com", "New book in library: 'The Trial' written by Kafka");
verify(notificationService, never()).sendNotification("bob@bob.com", "New book in library: 'The Trial' written by Kafka");
// Verify we stored the book in the database and call the readers service:
verify(libraryDal, times(1)).storeBook("The Trial", "Kafka");
verify(readersService, times(1)).newBookAdded("BookId_The Trial_Kafka", "The Trial", "Kafka"); 

Even though the readers service is the internal element in the flow, the Spring injection mechanism allows us to manipulate its behavior. We not only verified that Alice received the notification and Bob didn’t (black box testing), but we also performed white box testing and verified the correct behavior inside the flow: that the book was stored in the database, and that we called our readers service correctly.

Going with the entire flow

Testing is a constant in the development and project lifecycle. We illustrated two Java testing methodologies. The first tests only the class, using Mockito, and the second uses a combination of Mockito with Spring to test entire flows.

Spring lets you leverage your team’s tests, ensuring not only that the quality is tested but that the application’s functionality is tested as well. A test that verifies that given an input x the class produces output y is important and indicates good quality. Still, good quality does not ensure that the flow itself works as defined, and this is why it is also important to test the entire flow.

Our experience with testing entire flows shows us that sometimes a test of a flow can help us change and fix the behavior of a certain unit (class) in it.

Although using Spring together with Mockito has its drawbacks, mainly with the complexity of the test and the need for all team members to be familiar with both Mockito and Spring, the effort of adjusting this kind of test is worthwhile in the end.

Image credit: Flickr

Keep learning

Read more articles about: App Dev & TestingApp Dev