Computer scienceBackendSpring BootSpring Testing

Testing Beans

11 minutes read

There are two ways to test beans in Spring: testing a bean like a usual Java or Kotlin class using JUnit and a mocking library or using special Spring techniques for testing. The fact that beans exist in the application context and usually have dependencies leads to a new testing approach. For example, the @Mock annotation is used in simple Mockito (org.mockito package) for testing usual Java or Kotlin classes, and @MockBean in Spring Mockito (org.springframework.boot.test.mock.mockito package) for beans in the context. Let's look at these two approaches and their use cases.

Preparation

In this topic, we will look at testing beans with a small example about books. So, first of all, let's create a Book class:

public class Book {

    private Long id;
    private String name;
    private String author;
    private int pageCount;

    // getters, setters, constructor
}

There is also a BookService component that just simulates the selection of all books from the database by a specific author.

@Component
public class BookService {

    public List<Book> getBooksByAuthor(String author) {
        // should access db and return all books with a specific author
        // but in our example it just will return a hard-coded list of books
        return List.of(
                new Book("title1", author, 100),
                new Book("title2", author, 200),
                new Book("title3", author, 300));
    }
}

The next component is a BookStatisticsService that uses the BookService inside its getTotalPagesByAuthor method. This method summarizes all pages of books by a certain author.

@Component
public class BookStatisticsService {

    private final BookService bookService;

    @Autowired
    public BookStatisticsService(BookService bookService) {
        this.bookService = bookService;
    }

    public int getTotalPagesByAuthor(String author) {
        return bookService.getBooksByAuthor(author)
                .stream()
                .mapToInt(Book::getPageCount)
                .sum();
    }
}

So, the goal is to write a test that checks that the getTotalPagesByAuthor method works correctly.

Testing with dependencies

If we tested the getTotalPagesByAuthor method without Spring, we would create a test like this:

import static org.assertj.core.api.Assertions.assertThat;

public class BookStatisticsServiceTest {
    private BookService bookService = new BookService();
    private BookStatisticsService bookStatisticsService = new BookStatisticsService(bookService);

    @Test
    void shouldReturnTotalPages() {
        int actual = bookStatisticsService.getTotalPagesByAuthor("Jane Austen");
        int expected = 600;
        assertThat(actual).isEqualTo(expected);
    }
}

It's fast and simple, but Spring relies heavily on dependency injection, and in most cases, a bean can have many dependencies. So it is inefficient to create and inject all the objects into other objects manually.

For such cases, it would be better to set up an application context for tests:

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class BookStatisticsServiceContextTest {

    @Autowired
    private BookStatisticsService bookStatisticsService;

    @Test
    void shouldReturnTotalPages() {
        int actual = bookStatisticsService.getTotalPagesByAuthor("Jane Austen");
        int expected = 600;
        assertThat(actual).isEqualTo(expected);
    }
}

Here, it's worth noting that the context may contain objects we don't need for testing, so time and memory may be wasted on unnecessary objects. To improve the situation, you can create a new testing configuration by putting all required beans there or use test slices (which will be discussed in a separate topic).

Test doubles

A test double is a simulation of a real component used for testing. The typical components test doubles simulate are external APIs or database interactions. For instance, consider a scenario where a tested bean accesses a database. Connection to the database takes a significant amount of time, but we are focusing on testing the behavior of that bean, not the connection. So, in this case, replacing the component responsible for database interactions with a lightweight imitating object (test double) would be optimal.

There are several types of test doubles: mocks, spies, stubs, and fakes. In this topic, we will discuss mocks and the Mockito library that will be used in Java examples. Mockito helps in creating and configuring mock objects in Java. Its main functionalities include:

  • The @Mock annotation is used to create a mock object in the test class;

  • @InjectMocks is used to automatically inject mock objects into the fields of a tested object;

  • The when(...).thenReturn(...) statement specifies behavior when a particular method is called on a mock object.

The documentation for Mockito can be found here.

Also, Mockito is integrated into the Spring Test Framework, which provides additional functionality. For example, Spring Mockito introduces the @MockBean annotation that is used to mock beans in a Spring application context.

Mocking

In our example, the BookService component is nice and simple, but in an actual application, it will be heavy as it connects to a database. So, in this case, it would be better to replace the BookService with a mocking object.

Let's do this with the usual Mockito and JUnit:

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.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(MockitoExtension.class)
public class BookStatisticsServiceMockTest {
    @Mock
    BookService bookService;

    @InjectMocks
    BookStatisticsService bookStatisticsService;

    @Test
    void shouldReturnTotalPages() {

        List<Book> defaultReturnValue = bookService.getBooksByAuthor("Jane Austen");

        // by default the mocked method returns an empty collection
        assertThat(defaultReturnValue).hasSize(0);

        // using when..thenReturn statement, we change the default behavior of the mock object
        List<Book> janeAustenBooks = List.of(
                new Book("Pride and Prejudice", "Jane Austen", 259),
                new Book("Emma", "Jane Austen", 218));

        Mockito.when(bookService.getBooksByAuthor("Jane Austen"))
                .thenReturn(janeAustenBooks);

        int actual = bookStatisticsService.getTotalPagesByAuthor("Jane Austen");
        int expected = 218 + 259;
        assertThat(actual).isEqualTo(expected);
    }
}

With the @Mock annotation, we mark the object bookService that will be replaced by another imitating object (mock). Since bookStatisticsService depends on bookService, we inject the mock into the bookStatisticsService using the @InjectMocks annotation.

By default, depending on the return type of the original method, mocked object method will return null, 0, false, or empty collection. In our example, the original getBooksByAuthor method returns a collection, which means the mocked object method will return an empty collection by default.

To make it return the collection you need, you can use when(...).thenReturn(...) construction from Mockito. In our example, when the method is invoked with the parameter "Jane Austen", it returns a created janeAustenBooks list. During the execution of the getTotalPagesByAuthor method, a mocked getBooksByAuthor method is called. So the operations inside getTotalPagesByAuthor are performed on the janeAustenBooks list, and we get the expected result.

Another way to check the same method is to use Mockito from the Spring Boot package:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class BookStatisticsServiceContextMockTest {

    @MockBean
    private BookService bookService;

    @Autowired
    private BookStatisticsService bookStatisticsService;

    @Test
    void shouldReturnTotalPages() {
        List<Book> defaultReturnValue = bookService.getBooksByAuthor("Jane Austen");

        // by default the mocked method returns an empty collection
        assertThat(defaultReturnValue).hasSize(0);

        // using when..thenReturn statement, we change the default behavior of the mock object
        List<Book> janeAustenBooks = List.of(
                new Book("Pride and Prejudice", "Jane Austen", 259),
                new Book("Emma", "Jane Austen", 218));

        Mockito.when(bookService.getBooksByAuthor("Jane Austen"))
                .thenReturn(janeAustenBooks);

        int actual = bookStatisticsService.getTotalPagesByAuthor("Jane Austen");
        int expected = 259 + 218;
        assertThat(actual).isEqualTo(expected);
    }
}

Instead of the @Mock, we use the @MockBean annotation that replaces the bean in the application context with an imitating object. Since the application context stores not only beans but also their connections, the imitating object will be automatically injected into the required beans.

In our example, we mocked the bookService bean in the context and then injected the bookStatisticsService bean from the context into the test class. Here, the test method does the same thing as the test method of the BookStatisticsServiceMockTest class.

@MockBean replaces any existing bean of the same type in the application context. if there is no such bean in the context, a new one will be added. So, @MockBean changes the context by adding or replacing beans.

So, we've discussed two approaches: using the usual Mockito and using Mockito from Spring. The first one is faster, so why should we use the second one with the application context? Spring Mockito simplifies testing in situations where you need to mock a bean that is far away from the tested component in the dependency tree. Let's imagine we have six beans connected and want to mock bean5 and test bean1. Application context and test application context would look like this:

Thanks to the application context, which preserves connections between beans, in the test with Spring, we would write only this:

@SpringBootTest
public class ManyBeansContextTest {

    @MockBean
    private Bean5 bean5;
    
    @Autowired
    private Bean1 bean1;
    
    // test method
}

While the usual Mockito needs building dependencies:

public class ManyBeansContextTest {

    @Test
    void shouldReturnSomething(){
        Bean5 mock = Mockito.mock(Bean5.class);
        Mockito.when(mock.method()).thenReturn("MOCK");
        Bean6 bean6 = new Bean6();
        Bean3 bean3 = new Bean3(mock, bean6);
        Bean4 bean4 = new Bean4();
        Bean2 bean2 = new Bean2(bean4);
        Bean1 bean1 = new Bean1(bean2, bean3);
        ...
    }
}

Conclusion

Since usual unit tests are fast and straightforward, it's recommended to use them when classes don't rely heavily on application context or their dependencies can be easily mocked, for example, for testing business logic of isolated components. Also, if you use Spring, it doesn't mean you need to store every object in the application context. Use regular Java or Kotlin classes when convenient, as their testing and maintenance are often easier. However, Spring provides good tools for more DI-related cases to simplify dependency management in tests. With their help, objects from the application context can be more easily mocked and tested.

37 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo