Module 11 - Testing in Spring Boot (JUnit & MockMvc)


Learning Objectives

By the end of this module, you will be able to:

  1. Understand the importance of automated testing in software development
  2. Distinguish between different types of tests: unit, integration, and end-to-end
  3. Use JUnit 5 to write effective unit tests
  4. Implement integration tests with SpringBootTest
  5. Test REST controllers using MockMvc
  6. Apply test slices for focused testing
  7. Use Mockito to mock dependencies
  8. Implement and understand test-driven development (TDD)
  9. Create and use test fixtures and test utilities
  10. Generate test reports and analyze test coverage

Part 1: Understanding Testing in Spring Boot

Why Testing Matters

Testing is a critical part of software development that helps ensure your code works as expected and continues to work as you make changes. For Spring Boot applications, comprehensive testing is especially important because these applications often serve as mission-critical systems with complex business logic.

Types of Tests

Unit Tests

  • Test individual components in isolation
  • Fast execution, focused scope
  • Usually mock dependencies
  • Test a single “unit” of functionality

Integration Tests

  • Test how components work together
  • More realistic testing scenarios
  • Often involve Spring context loading
  • May use in-memory databases

End-to-End Tests

  • Test the complete application flow
  • Often involve UI testing
  • Slower but more comprehensive
  • Test real user scenarios

Spring Boot Testing Stack

Spring Boot provides excellent support for testing through:

  1. Spring Boot Test: Comprehensive testing support with auto-configuration
  2. JUnit 5: The foundation testing framework
  3. MockMvc: For testing web controllers without starting a server
  4. WebTestClient: For testing reactive web applications
  5. TestRestTemplate: For testing REST endpoints with a running server
  6. @DataJpaTest: For testing JPA repositories
  7. Mockito: For creating mock objects in unit tests

Key Spring Boot Testing Components

Spring Boot provides excellent support for testing through various libraries and annotations:

1. JUnit 5 (Jupiter)

JUnit is the most popular testing framework for Java. Spring Boot 2.2+ uses JUnit 5 by default, which offers:

  • Improved test lifecycle management
  • Better parameterized tests
  • Extension model instead of runners
  • Nested test classes for organizing related tests
  • Better assertion messages and reporting

2. Spring Test Framework

Spring’s test framework helps you test Spring applications with:

  • @SpringBootTest - Loads the entire application context
  • @WebMvcTest - Loads only the web layer
  • @DataJpaTest - Tests JPA repositories
  • @MockBean and @SpyBean - Replace or wrap beans with test doubles
  • TestRestTemplate and WebTestClient - Make HTTP requests to your application

3. MockMvc

MockMvc allows you to test your controllers without starting a real HTTP server:

  • Tests Spring MVC controllers in isolation
  • Simulates HTTP requests and validates responses
  • Fast and reliable
  • Offers a fluent API for request building and result verification

4. Mockito

Mockito is a mocking framework that helps you create test doubles:

  • Mocks - Objects with predefined behavior
  • Spies - Partial mocks that can call real methods
  • Verifications - Assert that specific methods were called
  • Argument matchers - Flexible matching of method parameters

Part 2: Setting Up the Testing Environment

JUnit 5 Overview

JUnit 5 consists of several modules:

  1. JUnit Platform: The foundation for launching testing frameworks on the JVM
  2. JUnit Jupiter: The programming and extension model for writing tests
  3. JUnit Vintage: Support for running JUnit 3 and JUnit 4 tests

Essential Annotations

  • @Test: Marks a method as a test method
  • @BeforeEach: Executes before each test method
  • @AfterEach: Executes after each test method
  • @BeforeAll: Executes once before all test methods in the class
  • @AfterAll: Executes once after all test methods in the class
  • @DisplayName: Provides a custom name for the test
  • @Disabled: Disables a test method or class

Assertions

JUnit 5 provides a rich set of assertion methods:

assertEquals(expected, actual)
assertTrue(condition)
assertFalse(condition)
assertNotNull(object)
assertThrows(ExpectedException.class, () -> { /* code */ })
assertAll(() -> assertion1, () -> assertion2, ...)

Part 3: Unit Testing in Spring Boot

Testing Service Layer

When testing the service layer, we focus on testing business logic without worrying about external dependencies like databases. We use mocks to isolate the code under test.

Example of testing a service:

@ExtendWith(MockitoExtension.class)
class BookServiceTest {

    @Mock
    private BookRepository bookRepository;

    @InjectMocks
    private BookServiceImpl bookService;

    @Test
    @DisplayName("Should find book by ID")
    void shouldFindBookById() {
        // Arrange
        Long bookId = 1L;
        Book expectedBook = new Book(bookId, "Test Book", "Test Author", "1234567890");
        when(bookRepository.findById(bookId)).thenReturn(Optional.of(expectedBook));

        // Act
        Book actualBook = bookService.findById(bookId);

        // Assert
        assertEquals(expectedBook, actualBook);
        verify(bookRepository).findById(bookId);
    }

    @Test
    @DisplayName("Should throw exception when book not found")
    void shouldThrowExceptionWhenBookNotFound() {
        // Arrange
        Long bookId = 1L;
        when(bookRepository.findById(bookId)).thenReturn(Optional.empty());

        // Act & Assert
        assertThrows(BookNotFoundException.class, () -> bookService.findById(bookId));
        verify(bookRepository).findById(bookId);
    }
}

Testing Repositories

For testing repositories, Spring Boot provides @DataJpaTest which:

  • Configures an in-memory database
  • Sets up Spring Data JPA
  • Disables full auto-configuration
  • Scans for @Entity classes and Spring Data JPA repositories

Example of testing a repository:

@DataJpaTest
class BookRepositoryTest {

    @Autowired
    private BookRepository bookRepository;

    @Test
    @DisplayName("Should find book by ISBN")
    void shouldFindBookByISBN() {
        // Arrange
        String isbn = "1234567890";
        Book book = new Book(null, "Test Book", "Test Author", isbn);
        bookRepository.save(book);

        // Act
        Optional<Book> foundBook = bookRepository.findByIsbn(isbn);

        // Assert
        assertTrue(foundBook.isPresent());
        assertEquals(isbn, foundBook.get().getIsbn());
    }
}

Part 4: Testing Web Controllers with MockMvc

MockMvc allows you to test your controllers without starting a server. It’s perfect for testing:

  • Request mapping
  • Request parameters
  • Response status codes
  • Response content

Example of testing a controller:

@WebMvcTest(BookController.class)
class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BookService bookService;

    @Test
    @DisplayName("Should return book by ID")
    void shouldReturnBookById() throws Exception {
        // Arrange
        Long bookId = 1L;
        Book book = new Book(bookId, "Test Book", "Test Author", "1234567890");
        when(bookService.findById(bookId)).thenReturn(book);

        // Act & Assert
        mockMvc.perform(get("/api/books/{id}", bookId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(bookId))
                .andExpect(jsonPath("$.title").value("Test Book"))
                .andExpect(jsonPath("$.author").value("Test Author"))
                .andExpect(jsonPath("$.isbn").value("1234567890"));

        verify(bookService).findById(bookId);
    }

    @Test
    @DisplayName("Should return 404 when book not found")
    void shouldReturn404WhenBookNotFound() throws Exception {
        // Arrange
        Long bookId = 1L;
        when(bookService.findById(bookId)).thenThrow(new BookNotFoundException(bookId));

        // Act & Assert
        mockMvc.perform(get("/api/books/{id}", bookId))
                .andExpect(status().isNotFound());

        verify(bookService).findById(bookId);
    }
}

Part 5: Integration Testing

Integration tests verify that different parts of your application work together correctly. In Spring Boot, we use @SpringBootTest for integration testing.

Example of an integration test:

@SpringBootTest
@AutoConfigureMockMvc
class BookIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private BookRepository bookRepository;

    @BeforeEach
    void setup() {
        bookRepository.deleteAll();
    }

    @Test
    @DisplayName("Should create and retrieve a book")
    void shouldCreateAndRetrieveBook() throws Exception {
        // Create a book
        Book book = new Book(null, "Integration Test Book", "Test Author", "9876543210");
        String bookJson = new ObjectMapper().writeValueAsString(book);

        MvcResult result = mockMvc.perform(post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content(bookJson))
                .andExpect(status().isCreated())
                .andReturn();

        String location = result.getResponse().getHeader("Location");
        assertNotNull(location);

        // Retrieve the book
        mockMvc.perform(get(location))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("Integration Test Book"))
                .andExpect(jsonPath("$.author").value("Test Author"))
                .andExpect(jsonPath("$.isbn").value("9876543210"));
    }
}

Part 6: Test-Driven Development (TDD)

Test-Driven Development is a software development process where you:

  1. Write a failing test
  2. Write minimal code to make the test pass
  3. Refactor the code while keeping the tests passing

Benefits of TDD:

  • Ensures code is testable from the start
  • Provides immediate feedback on code quality
  • Serves as documentation for how code should work
  • Reduces debugging time
  • Encourages simpler designs

Part 7: Advanced Testing Techniques

Parameterized Tests

JUnit 5 supports parameterized tests, allowing you to run a test multiple times with different parameters:

@ParameterizedTest
@ValueSource(strings = {"1234567890", "0987654321", "5432167890"})
void isValidIsbn(String isbn) {
    assertTrue(bookService.isValidIsbn(isbn));
}

@ParameterizedTest
@CsvSource({
    "Test Book, Test Author, 1234567890",
    "Another Book, Another Author, 0987654321"
})
void createBook(String title, String author, String isbn) {
    Book book = new Book(null, title, author, isbn);
    Book savedBook = bookService.save(book);

    assertNotNull(savedBook.getId());
    assertEquals(title, savedBook.getTitle());
    assertEquals(author, savedBook.getAuthor());
    assertEquals(isbn, savedBook.getIsbn());
}

Test Fixtures

Test fixtures are fixed states used as a baseline for running tests. They can be implemented using:

  • @BeforeEach and @AfterEach methods
  • Test utility classes
  • Factory methods for creating test objects

Example of a test fixture:

class BookTestFixture {
    public static Book createTestBook() {
        return new Book(1L, "Test Book", "Test Author", "1234567890");
    }

    public static List<Book> createTestBooks() {
        return Arrays.asList(
            new Book(1L, "Test Book 1", "Test Author 1", "1234567890"),
            new Book(2L, "Test Book 2", "Test Author 2", "0987654321"),
            new Book(3L, "Test Book 3", "Test Author 3", "5432167890")
        );
    }
}

Test Slices

Spring Boot provides several test slice annotations that load only a specific part of the application context:

  • @WebMvcTest: For testing MVC controllers
  • @DataJpaTest: For testing JPA repositories
  • @JsonTest: For testing JSON serialization/deserialization
  • @RestClientTest: For testing REST clients
  • @WebFluxTest: For testing WebFlux controllers

Part 8: Test Coverage and Reporting

Test coverage measures how much of your code is executed during tests. It helps identify untested code paths.

Tools for test coverage:

  • JaCoCo: Java Code Coverage Library
  • Codecov: Cloud-based code coverage

Adding JaCoCo to your Spring Boot project:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.8</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Quiz

  1. Which annotation is used to mark a method as a test in JUnit 5? a) @TestMethod b) @UnitTest c) @Test d) @JUnitTest

  2. Which Spring Boot test annotation is used to test only the web layer? a) @WebTest b) @WebMvcTest c) @ControllerTest d) @MvcTest

  3. What is the purpose of Mockito in testing? a) To create a fake web server b) To generate test reports c) To create mock objects for dependencies d) To measure code coverage

  4. Which annotation is used to execute code before each test method? a) @Before b) @BeforeEach c) @Setup d) @Initialize

  5. What is Test-Driven Development (TDD)? a) Writing tests after implementing features b) Writing tests and code simultaneously c) Writing tests first, then implementing features to pass those tests d) Testing only the driver code in an application

  6. Which test type provides the most comprehensive verification but is typically slowest to run? a) Unit tests b) Integration tests c) End-to-end tests d) Performance tests

  7. What does the MockMvc class allow you to test? a) Database queries b) Service layer logic c) Web controllers without starting a server d) Asynchronous operations

  8. Which of these is NOT a benefit of automated testing? a) Improved code quality b) Faster development c) Reduced need for code reviews d) Early detection of bugs

  9. What is the purpose of JaCoCo in a Spring Boot project? a) To generate API documentation b) To measure code coverage c) To mock database connections d) To create test data

  10. Which annotation is used to test JPA repositories in isolation? a) @RepositoryTest b) @JPATest c) @DataJpaTest d) @DatabaseTest


Quiz Answers

  1. c) @Test
  2. b) @WebMvcTest
  3. c) To create mock objects for dependencies
  4. b) @BeforeEach
  5. c) Writing tests first, then implementing features to pass those tests
  6. c) End-to-end tests
  7. c) Web controllers without starting a server
  8. c) Reduced need for code reviews (this is not a benefit of automated testing)
  9. b) To measure code coverage
  10. c) @DataJpaTest

By Wahid Hamdi