Spring Boot单元测试的Mock

下面是一个Spring Boot单元测试的Mock示例,使用Mockito框架模拟依赖关系,不启动整个Spring容器(轻量级测试):

示例场景

  • UserService:业务服务类
  • UserRepository:数据访问层(被Mock的对象)
  • 测试目标:UserService的业务逻辑

代码实现

1. 实体类

// src/main/java/com/example/demo/model/User.java
public class User {
    private Long id;
    private String name;
    private String email;
    
    // 构造方法/getters/setters
}

2. Repository接口

// src/main/java/com/example/demo/repository/UserRepository.java
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    boolean existsByEmail(String email);
}

3. Service实现类(无接口)

// src/main/java/com/example/demo/service/UserService.java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    
    private final UserRepository userRepository;

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

    @Transactional
    public User createUser(String name, String email) {
        if (userRepository.existsByEmail(email)) {
            throw new IllegalArgumentException("Email already exists");
        }
        
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        return userRepository.save(user);
    }

    public User getUserById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
    }
}

4. 单元测试类

// src/test/java/com/example/demo/service/UserServiceTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) // 启用Mockito
class UserServiceTest {

    @Mock
    private UserRepository userRepository; // 被Mock的依赖

    @InjectMocks
    private UserService userService; // 被测试的服务,自动注入Mock对象

    @Test
    void createUser_Success() {
        // 1. 准备测试数据
        String name = "John";
        String email = "john@example.com";
        User savedUser = new User(1L, name, email);

        // 2. 定义Mock行为
        when(userRepository.existsByEmail(email)).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // 3. 执行测试方法
        User result = userService.createUser(name, email);

        // 4. 验证结果
        assertNotNull(result);
        assertEquals(1L, result.getId());
        assertEquals(email, result.getEmail());
        
        // 5. 验证Mock交互
        verify(userRepository).existsByEmail(email);
        verify(userRepository).save(any(User.class));
    }

    @Test
    void createUser_WhenEmailExists_ShouldThrowException() {
        // 准备测试数据
        String email = "duplicate@example.com";

        // 定义Mock行为
        when(userRepository.existsByEmail(email)).thenReturn(true);

        // 执行并验证异常
        IllegalArgumentException exception = assertThrows(
            IllegalArgumentException.class,
            () -> userService.createUser("Alice", email)
        );

        assertEquals("Email already exists", exception.getMessage());
        verify(userRepository, never()).save(any());
    }

    @Test
    void getUserById_Found() {
        // 准备测试数据
        Long userId = 1L;
        User mockUser = new User(userId, "Bob", "bob@example.com");

        // 定义Mock行为
        when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));

        // 执行测试方法
        User result = userService.getUserById(userId);

        // 验证结果
        assertEquals(userId, result.getId());
        assertEquals("Bob", result.getName());
    }

    @Test
    void getUserById_NotFound() {
        // 准备测试数据
        Long userId = 999L;

        // 定义Mock行为
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // 执行并验证异常
        RuntimeException exception = assertThrows(
            RuntimeException.class,
            () -> userService.getUserById(userId)
        );

        assertEquals("User not found", exception.getMessage());
    }
}

关键点解析

  1. 测试框架

    • @ExtendWith(MockitoExtension.class):启用Mockito
    • 不需要@SpringBootTest,不启动Spring容器
  2. Mock对象

    • @Mock:创建模拟依赖(如UserRepository
    • @InjectMocks:创建被测对象并注入模拟依赖
  3. Mock行为配置

    • when(mock.method()).thenReturn(value):定义方法返回值
    • when(mock.method()).thenThrow(exception):定义方法抛出异常
  4. 验证交互

    • verify(mock).method():验证方法被调用
    • verify(mock, times(n)).method():验证调用次数
    • verify(mock, never()).method():验证从未调用
  5. 断言

    • assertEquals:验证值相等
    • assertThrows:验证抛出预期异常
    • assertNotNull:验证非空

优势

  1. 超快执行速度:不启动Spring容器,测试在毫秒级完成
  2. 精准测试:只测试当前类的业务逻辑
  3. 完全控制:可以模拟各种边界情况和异常场景
  4. 无副作用:不操作真实数据库,无数据污染

运行测试

在IDE中直接运行测试类,或使用Maven命令:

mvn test

最佳实践:对于Service层的业务逻辑测试,推荐这种Mock方式而非集成测试。只有当需要测试数据库真实交互、事务传播等场景时,才使用@DataJpaTest@SpringBootTest启动容器。

posted @ 2025-06-04 23:01  cqs1234  阅读(76)  评论(0)    收藏  举报