Module 11 - Testing in Spring Boot (JUnit & MockMvc)
Learning Objectives
By the end of this module, you will be able to:
- Understand the importance of automated testing in software development
- Distinguish between different types of tests: unit, integration, and end-to-end
- Use JUnit 5 to write effective unit tests
- Implement integration tests with SpringBootTest
- Test REST controllers using MockMvc
- Apply test slices for focused testing
- Use Mockito to mock dependencies
- Implement and understand test-driven development (TDD)
- Create and use test fixtures and test utilities
- 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:
- Spring Boot Test: Comprehensive testing support with auto-configuration
- JUnit 5: The foundation testing framework
- MockMvc: For testing web controllers without starting a server
- WebTestClient: For testing reactive web applications
- TestRestTemplate: For testing REST endpoints with a running server
- @DataJpaTest: For testing JPA repositories
- 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 doublesTestRestTemplate
andWebTestClient
- 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:
- JUnit Platform: The foundation for launching testing frameworks on the JVM
- JUnit Jupiter: The programming and extension model for writing tests
- 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:
- Write a failing test
- Write minimal code to make the test pass
- 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
-
Which annotation is used to mark a method as a test in JUnit 5? a) @TestMethod b) @UnitTest c) @Test d) @JUnitTest
-
Which Spring Boot test annotation is used to test only the web layer? a) @WebTest b) @WebMvcTest c) @ControllerTest d) @MvcTest
-
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
-
Which annotation is used to execute code before each test method? a) @Before b) @BeforeEach c) @Setup d) @Initialize
-
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
-
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
-
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
-
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
-
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
-
Which annotation is used to test JPA repositories in isolation? a) @RepositoryTest b) @JPATest c) @DataJpaTest d) @DatabaseTest
Quiz Answers
- c) @Test
- b) @WebMvcTest
- c) To create mock objects for dependencies
- b) @BeforeEach
- c) Writing tests first, then implementing features to pass those tests
- c) End-to-end tests
- c) Web controllers without starting a server
- c) Reduced need for code reviews (this is not a benefit of automated testing)
- b) To measure code coverage
- c) @DataJpaTest
By Wahid Hamdi