@MockBean in Spring Boot Testing: CRUD Operations Example

In Spring Boot, @MockBean is a powerful annotation used for testing, particularly when dealing with external dependencies or layers that need to be isolated. This blog post will demonstrate how to effectively use @MockBean in Spring Boot for testing CRUD operations. 

Understanding @MockBean in Spring Boot 

@MockBean is an annotation provided by Spring Boot for testing purposes. It is used to add mock objects to the Spring application context or replace existing beans with their mock versions during the execution of tests. This is especially useful in integration testing where a part of the application needs to be tested in isolation. 

Why Use @MockBean? 

Isolation: It allows for testing a specific part of the application in isolation, particularly when external services are involved. 

Controlled Environment: Enables the creation of a controlled testing environment by defining the expected behavior of the mock. 

Integration Testing: Facilitates testing within the full Spring context without relying on external dependencies. 

A CRUD Operations Example with @MockBean 

Let's see an example demonstrating CRUD operations in a user management system. We'll create a User entity, a repository, a service, and a controller, and then write tests using @MockBean

The User Entity

import jakarta.persistence.*;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    private String email;

    // Constructors, Getters, Setters
}

The UserRepository

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

The UserService

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User createUser(User user) {
        return userRepository.save(user);
    }

    public Optional<User> getUser(Long id) {
        return userRepository.findById(id);
    }

    public User updateUser(Long id, User userDetails) {
        User user = userRepository.findById(id).orElseThrow();
        user.setFirstName(userDetails.getFirstName());
        user.setLastName(userDetails.getLastName());
        user.setEmail(userDetails.getEmail());
        return userRepository.save(user);
    }

    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}

The UserController

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@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) {
        return ResponseEntity.ok(userService.createUser(user));
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userService.getUser(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        return ResponseEntity.ok(userService.getAllUsers());
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        return ResponseEntity.ok(userService.updateUser(id, user));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.ok().build();
    }
}

UserServiceTest - Testing with @MockBean

The below CRUD tests will demonstrate how to mock and assert the behavior of the UserService using @MockBean for the UserRepository.

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.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;

@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserRepository userRepository;

    @Test
    public void testCreateUser() {
        User mockUser = new User(1L, "John", "Doe", "john@example.com");
        Mockito.when(userRepository.save(any(User.class))).thenReturn(mockUser);

        User result = userService.createUser(new User());
        assertEquals(mockUser, result);
    }

    @Test
    public void testGetUserById() {
        Long userId = 1L;
        User mockUser = new User(userId, "John", "Doe", "john@example.com");
        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));

        Optional<User> result = userService.getUserById(userId);
        assertTrue(result.isPresent());
        assertEquals(mockUser, result.get());
    }

    @Test
    public void testUpdateUser() {
        Long userId = 1L;
        User existingUser = new User(userId, "John", "Doe", "john@example.com");
        User updatedInfo = new User(userId, "Jane", "Doe", "jane@example.com");

        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser));
        Mockito.when(userRepository.save(any(User.class))).thenReturn(updatedInfo);

        User result = userService.updateUser(userId, updatedInfo);
        assertEquals(updatedInfo.getFirstName(), result.getFirstName());
        assertEquals(updatedInfo.getLastName(), result.getLastName());
        assertEquals(updatedInfo.getEmail(), result.getEmail());
    }

    @Test
    public void testDeleteUser() {
        Long userId = 1L;
        Mockito.doNothing().when(userRepository).deleteById(eq(userId));
        userService.deleteUser(userId);

        Mockito.verify(userRepository, Mockito.times(1)).deleteById(userId);
    }
}

Explanation:

testGetUserById: This test verifies that the getUserById method in UserService correctly retrieves a user by ID using the mocked UserRepository. 

testUpdateUser: This test checks if the updateUser method in UserService correctly updates a user's information and returns the updated user. 

testDeleteUser: This test ensures that the deleteUser method in UserService correctly invokes the deleteById method on the UserRepository.

By mocking the UserRepository and defining its behavior, we're able to isolate and test the UserService methods effectively, ensuring that they perform as expected without the need for actual database interaction. This approach is vital for writing reliable and maintainable unit tests in Spring Boot applications.

UserContollerTest - Testing with @MockBean

To test the UserController in a Spring Boot application, we'll use MockMvc which simulates HTTP requests to the controller and allows us to assert the responses. We will mock the UserService to isolate the controller layer. Let's write test cases for each CRUD operation in the UserControllerTest class.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
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.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ExtendWith(SpringExtension.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    private User sampleUser;

    @BeforeEach
    void setUp() {
        sampleUser = new User(1L, "John", "Doe", "john@example.com");
    }

    @Test
    public void createUserTest() throws Exception {
        Mockito.when(userService.createUser(Mockito.any(User.class))).thenReturn(sampleUser);

        mockMvc.perform(post("/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"firstName\":\"John\",\"lastName\":\"Doe\",\"email\":\"john@example.com\"}"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.firstName").value("John"))
               .andExpect(jsonPath("$.lastName").value("Doe"));
    }

    @Test
    public void getUserByIdTest() throws Exception {
        Mockito.when(userService.getUserById(sampleUser.getId())).thenReturn(Optional.of(sampleUser));

        mockMvc.perform(get("/users/" + sampleUser.getId()))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.firstName").value("John"))
               .andExpect(jsonPath("$.email").value("john@example.com"));
    }

    @Test
    public void updateUserTest() throws Exception {
        User updatedUser = new User(sampleUser.getId(), "Jane", "Doe", "jane@example.com");
        Mockito.when(userService.updateUser(Mockito.eq(sampleUser.getId()), Mockito.any(User.class))).thenReturn(updatedUser);

        mockMvc.perform(put("/users/" + sampleUser.getId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"email\":\"jane@example.com\"}"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.firstName").value("Jane"))
               .andExpect(jsonPath("$.email").value("jane@example.com"));
    }

    @Test
    public void deleteUserTest() throws Exception {
        Mockito.doNothing().when(userService).deleteUser(sampleUser.getId());

        mockMvc.perform(delete("/users/" + sampleUser.getId()))
               .andExpect(status().isOk());
    }
}

In these tests, MockMvc is used to simulate HTTP requests to the UserController, and UserService is mocked using @MockBean to ensure the tests are isolated to the controller layer. This approach allows us to verify that the UserController handles requests correctly and returns the appropriate responses for each CRUD operation.

Comments