Mastering Mocks: Stop Your Controller Tests from Flaking
- Published on
Mastering Mocks: Stop Your Controller Tests from Flaking
In the world of software development, testing is a key component that ensures our code behaves as expected. However, flaking tests can often lead to frustration, wasted effort, and can undermine confidence in our test suite. One of the primary culprits behind flaky tests in Java applications—especially with frameworks like Spring—is the improper management of dependencies in controller tests. In this blog post, we will delve into how to master mocks effectively, thus allowing you to create reliable, repeatable, and trustworthy tests.
The Problem with Flaky Tests
Flaky tests are tests that pass sometimes and fail at other times without any changes being made to the code. This inconsistency can obscure the true state of your application and lead to developers ignoring tests altogether. Flaky tests generally fall into different categories, such as:
- Timing Issues: Tests that rely on timing can fail if execution takes longer than expected.
- State Dependency: Tests that depend on the state of the application or external systems may fail if those states change unexpectedly.
- Randomness and Non-Determinism: Tests that involve elements of randomness will not always yield the same outcome.
In the context of controller tests, one of the worst offenders is the way we handle dependencies. Let’s explore how to mitigate this issue using mocks.
What Are Mocks?
Mocks are simulated objects or classes that mimic the behavior of real objects, allowing you to test components in isolation. By using mocks, you can assert the interactions between the controller and the service layer without relying on the actual service implementations, leading to more accurate and stable tests.
Why Use Mocks in Controller Testing?
-
Isolation: Mocks allow you to evaluate your controller’s logic without worrying about the actual business logic present in the services.
-
Control: With mocks, you can manipulate the behavior of dependencies, making it easier to test various scenarios.
-
Speed: Mocks are typically faster than using real dependencies, especially when testing with external services or databases.
Now that we understand mocks and their advantages, let’s explore implementing them in a Spring Boot application.
Setting Up Your Testing Environment
To demonstrate effective mocking, we will use the popular testing framework Mockito alongside JUnit. Ensure that you include the following dependency in your pom.xml
for a Maven project:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
Also, ensure you have JUnit included in your project:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
Creating a Sample Controller
Let’s say we have a simple controller for managing users in a system:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
}
This controller has a dependency on UserService
. In its createUser
method, it calls the service to create a user and returns a response.
Writing a Test with Mocks
Now, let’s write a test for this controller using Mockito to mock the UserService
. This will ensure our test is independent of the service layer.
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testCreateUser() throws Exception {
User user = new User("john.doe@example.com", "John Doe");
User createdUser = new User(1L, "john.doe@example.com", "John Doe");
when(userService.createUser(any(User.class))).thenReturn(createdUser);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"john.doe@example.com\",\"name\":\"John Doe\"}"))
.andExpect(status().isCreated())
.andReturn();
verify(userService, times(1)).createUser(any(User.class));
}
}
Breakdown of the Test
-
Annotations:
@WebMvcTest
loads only theUserController
, making our test light and focused.@MockBean
creates a mock instance ofUserService
, enabling injection into our controller. -
Preparing the Mock: The
when
statement is used to configure the mock. It tells Mockito to return a specific user when thecreateUser
method is called with anyUser
object. -
Performing the Mocked Request: We create a mock HTTP POST request with a JSON representation of the user.
mockMvc.perform
executes the request. -
Assertions: We assert that the response status is
201 CREATED
. Additionally, we verify that thecreateUser
method of theuserService
was called exactly once.
This setup isolates our controller tests, ensuring they do not "flake" due to issues in the UserService
.
Advanced Mocking Techniques
Beyond simple interaction mocking, Mockito provides advanced features such as:
- Argument Captors: Capture the arguments passed to mocked methods.
- Spying: Create partial mocks that can call real methods while also forwarding calls to the mock.
- Throwing Exceptions: Simulate exceptions to test error-handling code paths.
Using these techniques can increase the robustness of your test suite.
Example of Argument Captors
import org.mockito.ArgumentCaptor;
@Test
public void testCreateUserCapturesUser() throws Exception {
User user = new User("john.doe@example.com", "John Doe");
when(userService.createUser(any(User.class))).thenReturn(user);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"john.doe@example.com\",\"name\":\"John Doe\"}"));
ArgumentCaptor<User> userArgumentCaptor = ArgumentCaptor.forClass(User.class);
verify(userService).createUser(userArgumentCaptor.capture());
User capturedUser = userArgumentCaptor.getValue();
assertEquals("john.doe@example.com", capturedUser.getEmail());
}
In this example, we use ArgumentCaptor
to capture the user passed to the createUser
method, allowing us to perform assertions on it.
To Wrap Things Up
Mastering mocks is a crucial skill for any Java developer, especially in ensuring your controller tests remain free from flakiness. By using mocks effectively, you can isolate unit tests, control dependencies, and build a reliable test suite that instills confidence in your codebase.
For more comprehensive mock testing, consider revisiting the Mockito Documentation and JUnit 5 Documentation.
As you integrate these strategies into your own testing practices, remember that the goal is not merely passing tests, but ultimately producing high-quality, maintainable code. Happy testing!