Chapter 3: Unit Testing with Spring Boot
This chapter provides a comprehensive understanding of unit testing principles, focusing on how to apply these concepts using Spring Boot, JUnit, and Mockito. It includes practical examples, best practices, a hands-on lab, and a quiz to reinforce learning.
Objective
By the end of this chapter, you will:
- Understand the importance of unit testing in software development.
- Learn how to write unit tests for Spring Boot applications using JUnit and Mockito.
- Master techniques for isolating units of code and mocking dependencies.
- Apply best practices for writing maintainable and effective unit tests.
3.1 Introduction to Unit Testing
Unit testing is the foundation of software testing, focusing on verifying the smallest testable parts of an application—typically methods or classes.
- Definition: Unit testing involves testing individual components in isolation to ensure they work as expected, independent of other parts of the system.
- Purpose:
- Catch bugs early in the development cycle, reducing debugging time later.
- Ensure each unit of code performs correctly before integration.
- Provide a safety net for refactoring, preventing regressions.
- Key Characteristics:
- Isolation: Units are tested independently from external systems or dependencies.
- Automation: Tests are automated, allowing repeated execution without manual effort.
- Speed: Unit tests execute quickly, providing rapid feedback to developers.
Think of unit testing as checking each brick before building a wall—ensuring every piece is solid and reliable.
3.2 Tools for Unit Testing in Spring Boot
To perform unit testing in Spring Boot, we rely on two powerful tools:
- JUnit: A popular testing framework for Java that provides annotations (e.g.,
@Test
) and assertions (e.g.,assertEquals
) to write and run tests. - Mockito: A mocking framework that lets you create mock objects to simulate the behavior of dependencies, ensuring tests focus on the unit under test.
These tools integrate seamlessly with Spring Boot, making them ideal for testing Spring components like services and controllers.
3.3 Writing Unit Tests for Spring Boot Services
Services in Spring Boot often contain business logic and depend on repositories or other services. Unit testing these services requires isolating them from their dependencies using mocks.
3.3.1 Example Scenario
Consider a simple BookService that manages books:
@Service
public class BookService {
private final BookRepository bookRepository;
@Autowired
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public Book createBook(String title, String author) {
Book book = new Book(title, author);
return bookRepository.save(book);
}
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
}
3.3.2 Writing a Unit Test for createBook
To test the createBook
method, we:
- Mock the
BookRepository
to control its behavior. - Verify that
save
is called with the correct book object. - Check that the method returns the expected book.
Test Code:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
public class BookServiceTest {
@Test
public void testCreateBook() {
// Arrange
BookRepository mockRepo = mock(BookRepository.class);
BookService service = new BookService(mockRepo);
Book bookToSave = new Book("Test Title", "Test Author");
when(mockRepo.save(any(Book.class))).thenReturn(bookToSave);
// Act
Book result = service.createBook("Test Title", "Test Author");
// Assert
assertEquals("Test Title", result.getTitle());
assertEquals("Test Author", result.getAuthor());
verify(mockRepo).save(any(Book.class));
}
}
Explanation:
- Arrange: Set up the mock
BookRepository
and instantiateBookService
with it. Usewhen
to define the mock’s behavior. - Act: Call the
createBook
method with test data. - Assert: Check the returned book’s properties and verify the mock interaction.
3.3.3 Writing a Unit Test for getAllBooks
For getAllBooks
, we:
- Mock the repository to return a predefined list of books.
- Verify that
findAll
is called. - Check that the method returns the correct list.
Test Code:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
public class BookServiceTest {
@Test
public void testGetAllBooks() {
// Arrange
BookRepository mockRepo = mock(BookRepository.class);
List<Book> books = Arrays.asList(new Book("Book1", "Author1"), new Book("Book2", "Author2"));
when(mockRepo.findAll()).thenReturn(books);
BookService service = new BookService(mockRepo);
// Act
List<Book> result = service.getAllBooks();
// Assert
assertEquals(2, result.size());
assertEquals("Book1", result.get(0).getTitle());
verify(mockRepo).findAll();
}
}
3.4 Writing Unit Tests for Spring Boot Controllers
Controllers handle HTTP requests and responses. Unit testing them involves verifying they process input correctly and return the expected output, while mocking their dependencies.
3.4.1 Example Scenario
Consider a BookController that exposes endpoints for books:
@RestController
@RequestMapping("/books")
public class BookController {
private final BookService bookService;
@Autowired
public BookController(BookService bookService) {
this.bookService = bookService;
}
@PostMapping
public ResponseEntity<Book> createBook(@RequestBody Book book) {
Book createdBook = bookService.createBook(book.getTitle(), book.getAuthor());
return ResponseEntity.ok(createdBook);
}
@GetMapping
public ResponseEntity<List<Book>> getAllBooks() {
return ResponseEntity.ok(bookService.getAllBooks());
}
}
3.4.2 Writing a Unit Test for createBook
To test the createBook
endpoint:
- Mock the
BookService
. - Simulate a POST request using
MockMvc
. - Verify the response status and body.
Test Code:
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(BookController.class)
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookService bookService;
@Test
public void testCreateBook() throws Exception {
// Arrange
Book book = new Book("Test Title", "Test Author");
when(bookService.createBook(anyString(), anyString())).thenReturn(book);
// Act & Assert
mockMvc.perform(post("/books")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(book)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Test Title"))
.andExpect(jsonPath("$.author").value("Test Author"));
}
}
Explanation:
- @WebMvcTest: Loads only the web layer (controllers) for testing.
- MockMvc: Simulates HTTP requests to the controller.
- @MockBean: Injects a mock
BookService
into the controller.
3.4.3 Writing a Unit Test for getAllBooks
For getAllBooks
:
- Mock the service to return a list of books.
- Simulate a GET request.
- Verify the response contains the expected books.
Test Code:
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(BookController.class)
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookService bookService;
@Test
public void testGetAllBooks() throws Exception {
// Arrange
List<Book> books = Arrays.asList(new Book("Book1", "Author1"), new Book("Book2", "Author2"));
when(bookService.getAllBooks()).thenReturn(books);
// Act & Assert
mockMvc.perform(get("/books"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Book1"))
.andExpect(jsonPath("$[1].title").value("Book2"));
}
}
3.5 Best Practices for Unit Testing
To ensure your unit tests are effective and maintainable, follow these guidelines:
- Isolate Units: Use mocks to test each component independently of its dependencies.
- Test One Thing at a Time: Each test should verify a single behavior or scenario.
- Use Meaningful Names: Name tests descriptively (e.g.,
testCreateBook_Success
) to clarify their purpose. - Keep Tests Fast: Avoid slow operations like database calls; mocks help achieve this.
- Avoid Test Dependencies: Ensure tests are independent and can run in any order.
- Use Assertions Wisely: Focus assertions on the specific behavior being tested.
3.6 Lab: Hands-On Unit Testing with Spring Boot
Apply your knowledge in this practical lab.
Lab Objective
Write unit tests for a provided Spring Boot REST API using JUnit and Mockito.
Lab Setup
- Provided: A sample Spring Boot application with a
BookService
andBookController
similar to the examples above. - Tools: Maven or Gradle for dependency management, with JUnit and Mockito included.
Lab Tasks
-
Write Unit Tests for the Service Layer:
- Test
createBook
to ensure it saves a book and returns it correctly. - Test
getAllBooks
to verify it retrieves the list of books from the repository.
- Test
-
Write Unit Tests for the Controller Layer:
- Test the
createBook
endpoint to confirm it handles POST requests and returns the created book. - Test the
getAllBooks
endpoint to ensure it returns the list of books with a 200 status.
- Test the
-
Verify Mock Interactions:
- In service tests, ensure
save
andfindAll
are called on the repository as expected. - In controller tests, confirm the service methods are invoked correctly.
- In service tests, ensure
Lab Deliverables
- Submit your test classes with all unit tests.
- Include comments in your code explaining the purpose of each test.
3.7 Quiz: Reinforcing Key Concepts
Test your understanding with this quiz:
-
What is the primary goal of unit testing?
- a) To test the entire application
- b) To verify individual components in isolation
- c) To check integration between components
- d) To validate user interfaces
-
Which tool is used for mocking dependencies in Java?
- a) JUnit
- b) Mockito
- c) Spring Boot Test
- d) Rest Assured
-
In a unit test for a Spring Boot controller, what does @WebMvcTest do?
- a) Loads the full application context
- b) Loads only the web layer for testing
- c) Mocks the database
- d) Simulates HTTP requests
-
True or False: Unit tests should be slow to ensure thoroughness.
- a) True
- b) False
-
What is the purpose of the verify() method in Mockito?
- a) To check if a method was called on a mock object
- b) To assert the result of a test
- c) To create a mock object
- d) To run the test
Answers:
- b
- b
- b
- b (Unit tests should be fast)
- a
3.8 Summary
In this chapter, you’ve learned:
- The critical role of unit testing in ensuring code quality and reliability.
- How to use JUnit and Mockito to write unit tests for Spring Boot services and controllers.
- Techniques for isolating units and mocking dependencies to focus tests on specific behaviors.
- Best practices for creating maintainable, efficient, and effective unit tests.
These skills are essential for building robust Spring Boot applications and provide a strong foundation for exploring advanced testing techniques.
This chapter equips you with both the theoretical knowledge and practical skills to perform unit testing effectively in Spring Boot. You’re now prepared to tackle real-world testing challenges!
By Wahid Hamdi