Skip to content

单元测试


JUnit 5 + Mockito

Spring Boot 测试栈:

工具作用类比 PHP
JUnit 5测试框架PHPUnit
MockitoMock 对象Mockery
AssertJ断言库(assertThat)$this->assertEquals()
Spring TestSpring 集成测试Laravel TestCase

依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

加了这个依赖后,JUnit 5 + Mockito + AssertJ 都会自动引入。


单元测试(纯业务逻辑,不启动 Spring)

java
// src/test/java/com/example/myapp/service/UserServiceTest.java
class UserServiceTest {

    private UserService userService;

    private UserMapper userMapper;

    @BeforeEach                                    // 类比 setUp()
    void setUp() {
        userMapper = mock(UserMapper.class);        // 类比 Mockery::mock()
        userService = new UserService(userMapper);  // 手动组装
    }

    @Test                                           // 类比 @test 注解
    void shouldFindUserById() {                     // 使用方法名描述测试意图
        // given(准备数据)
        User mockUser = new User();
        mockUser.setId(1L);
        mockUser.setName("Test");
        when(userMapper.selectById(1L)).thenReturn(mockUser);

        // when(执行)
        User result = userService.findById(1L);

        // then(断言)
        assertThat(result.getName()).isEqualTo("Test");           // AssertJ 风格
        verify(userMapper).selectById(1L);                        // 验证 Mapper 被调用
    }

    @Test
    void shouldThrowWhenUserNotFound() {
        when(userMapper.selectById(999L)).thenReturn(null);

        assertThrows(ResourceNotFoundException.class,            // 预期异常
            () -> userService.findById(999L));
    }
}

切片测试(只启动部分 Spring 上下文)

Web 层测试

java
@WebMvcTest(UserController.class)        // 只启动 UserController 的 Spring 上下文
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;              // 模拟 HTTP 请求

    @MockBean                             // Mock UserService,不加载真实 Bean
    private UserService userService;

    @Test
    void shouldReturnUserList() throws Exception {
        // 准备 Mock 数据
        when(userService.findAll()).thenReturn(List.of(new User(1L, "Test")));

        // 发送 GET 请求并断言
        mockMvc.perform(get("/api/users")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].name").value("Test"));
        // 类比 PHP:
        // $response = $this->getJson('/api/users');
        // $response->assertStatus(200)->assertJson([...]);
    }
}

数据层测试

java
@DataJpaTest                                           // 只启动 JPA 相关组件
class UserRepositoryTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void shouldFindUserByEmail() {
        User user = new User("test@example.com", "Test");
        userMapper.insert(user);

        User found = userMapper.selectOne(
            new LambdaQueryWrapper<User>().eq(User::getEmail, "test@example.com"));

        assertThat(found).isNotNull();
        assertThat(found.getName()).isEqualTo("Test");
    }
}

全量集成测试(启动完整 Spring)

java
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)  // 启动完整应用
class IntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;    // 类比 $this->json()

    @Test
    void shouldCreateAndRetrieveUser() {
        // POST 创建
        var response = restTemplate.postForEntity(
            "/api/users",
            new CreateUserRequest("test@example.com", "Test"),
            User.class
        );
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        Long id = response.getBody().getId();

        // GET 查询
        var getResponse = restTemplate.getForEntity(
            "/api/users/{id}", User.class, id
        );
        assertThat(getResponse.getBody().getEmail())
            .isEqualTo("test@example.com");
    }
}

常用断言

java
// AssertJ(推荐,链式调用)
assertThat(result.getName())
    .isEqualTo("Test")
    .isNotBlank()
    .startsWith("T");

assertThat(resultList)
    .hasSize(3)
    .extracting(User::getName)
    .contains("Alice", "Bob");

assertThat(exception.getMessage())
    .contains("not found");

// JUnit 风格(传统)
assertEquals("Test", result.getName());
assertNotNull(result);
assertTrue(resultList.size() > 0);
assertThrows(ResourceNotFoundException.class, () -> userService.findById(999L));

⚠️ 常见坑

1. 测试类不加 public

JUnit 5 允许包级私有(不加 public),推荐使用默认包级私有以减少代码。

java
@Test
void shouldWork() { ... }     // ✅ JUnit 5 支持

2. @SpringBootTest 启动很慢

每次 @SpringBootTest 都会启动完整的 Spring 应用(3-10 秒)。尽量用 @WebMvcTest@DataJpaTest切片测试,只启动必要组件。

3. @MockBean 和 @Autowired 的区别

  • @Autowired:注入真实的 Spring Bean
  • @MockBean:用 Mockito Mock 替换掉真实的 Bean

切片测试中,未涉及的 Bean 会自动 Mock(避免启动完整的上下文)。

4. 测试目录结构

src/
  main/java/...       # 业务代码
  test/java/...       # 测试代码(包路径与 main 保持一致)

测试文件在 src/test/java/ 下,包路径和生产代码一致。不像 Laravel 的 tests/Feature/tests/Unit/

5. 没有 RefreshDatabase Trait

Laravel 的 use RefreshDatabase 在每个测试后回滚事务。Spring Boot 中用 @Transactional 实现:

java
@SpringBootTest
@Transactional     // 每个测试方法结束后自动回滚
class UserServiceTest { ... }

@Transactional@WebMvcTest 中不适用(Web 层测试的上下文不同)。

6. 测试命名规范

java
// 推荐:given_when_then 或 should_xxx 格式
@Test
void givenValidEmail_whenCreateUser_thenSuccess() { ... }

@Test
void shouldThrowExceptionWhenEmailDuplicated() { ... }

面向 PHP 开发者的 Spring Boot 文档