Spring Boot Test案例

wen java案例 2

构建高可用Spring Boot测试体系的实战指南(附完整案例)

目录导读

  1. 为什么你的Spring Boot测试总在“假绿”? —— 测试金字塔与真实性问题
  2. 环境准备与依赖哲学 —— 不只是引入spring-boot-starter-test
  3. Web层测试(@WebMvcTest) —— 隔离Controller,模拟Service
  4. 数据层测试(@DataJpaTest) —— 内置H2与真实MySQL的切换策略
  5. 集成测试(@SpringBootTest + Testcontainers) —— 告别“在我电脑上能跑”
  6. 进阶:MockMvc与AssertJ的优雅断言 —— 让测试代码可读如散文
  7. 性能与并发:@Transactional回滚陷阱 —— 99%的人会踩的坑
  8. QA高频问答 —— 解决你测试生涯的5个终极困惑

为什么你的Spring Boot测试总在“假绿”?

很多团队将测试当作“Code Coverage”的数字游戏,导致测试变成了虚假的安慰剂,真正的测试案例必须遵循测试金字塔:单元测试(70%)、集成测试(20%)、端到端测试(10%),如果你在写@SpringBootTest加载整个上下文去测一个UserServicesave()方法,你正在制造“慢如蜗牛且脆弱如纸”的测试。

Spring Boot Test案例

核心痛点:Spring Boot测试的难点在于上下文缓存外部依赖隔离,我们在本文中通过三个递进案例,教你构建一个测试运行时间<10秒、且不依赖真实网络的服务。

环境准备与依赖哲学

pom.xml中,很多人只写:

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

但这远远不够,实现优雅测试的隐藏依赖(必须添加):

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

哲学:用H2做快速逻辑验证,用Testcontainers做真实环境兼容验证,两者结合,既快又准。

案例一:Web层测试(@WebMvcTest)

场景:测试UserControllerGET /api/users/1接口。

核心代码

@WebMvcTest(UserController.class)
class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private UserService userService;
    @Test
    void whenGetUserById_thenReturnJson() throws Exception {
        User mockUser = new User(1L, "张三", "zhangsan@example.com");
        when(userService.findById(1L)).thenReturn(Optional.of(mockUser));
        mockMvc.perform(get("/api/users/1"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.name").value("张三"))
               .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }
}

关键点

  • 使用@MockBean切断了Service层的真实调用,只聚焦于Controller层的JSON序列化、HTTP状态码、参数校验
  • jsonPath是必须掌握的利器,它比getContentAsString().contains()更精准。

案例二:数据层测试(@DataJpaTest)

场景:验证UserRepository的自定义查询方法。

默认行为@DataJpaTest会自动启用嵌入式数据库(H2),并对每个测试事务回滚。

@DataJpaTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    void whenFindByEmail_thenReturnUser() {
        userRepository.save(new User("李四", "li@example.com"));
        Optional<User> found = userRepository.findByEmail("li@example.com");
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("李四");
    }
}

陷阱提示:如果你的实体类使用了@CreationTimestamp@UpdateTimestamp(Hibernate注解),在H2上可能会报方言错误,解决方案,在src/test/resources/application.yml中强制指定:

spring:
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect

案例三:集成测试(@SpringBootTest + Testcontainers)

为什么需要:H2与MySQL在锁行为、SQL方言、JSON函数上有差异,最典型的坑是LIKE查询在H2中默认不区分大小写,而在MySQL中区分。

高光代码——启动真实MySQL容器:

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTest {
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.33")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    @DynamicPropertySource
    static void dynamicProps(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }
    @Autowired
    private TestRestTemplate restTemplate;
    @Test
    void fullCRUD_whenUsingRealMysql_thenSuccess() {
        // 直接调用真实HTTP端口
        ResponseEntity<User> response = restTemplate.postForEntity(
            "/api/users", new User("王五", "wang@example.com"), User.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    }
}

核心价值@DynamicPropertySource动态覆盖数据源,让测试运行在一模一样的生产数据库版本上,彻底消灭“环境不一致”的甩锅事故。

进阶:MockMvc与AssertJ的优雅断言

不要使用JUnit原生的assertEquals一堆参数,用BDD风格的AssertJ

// 传统写法(可读性差)
assertEquals("张三", response.getName());
assertTrue(response.getAge() > 18);
// AssertJ优雅写法
assertThat(response.getName()).isEqualTo("张三");
assertThat(response.getAge()).isGreaterThan(18).isLessThan(60);

对于MockMvc,你可以通过andDo(print())在控制台输出完整的HTTP报文,调试时堪称“透视镜”。

性能与并发:@Transactional回滚陷阱

@WebMvcTest中,如果你对Service层方法标记了@Transactional,那么测试方法顶部也加上@Transactional,但这是危险的,因为一旦Controller不在事务控制范围内,当你多线程调用时,测试会因数据未提交而读到空值。

最佳实践

  • 绝不在Service层测试中手动flush()后再查询。
  • 对于并发测试,放弃@Transactional,使用@DirtiesContext或Testcontainers + 真实事务提交。

QA高频问答

Q1:测试的时候,能直接用System.out.println查看数据吗? A:可以,但低效,推介用@DataJpaTest测试时,配置spring.jpa.show-sql=true,日志输出更规范,若需看完整HTTP报文,用mockMvc.perform(...).andDo(print())

Q2:如果测试速度太慢,怎么定位是哪个Bean初始化导致的? A:在src/test/resources/application.yml中设置logging.level.org.springframework.boot.test.context=DEBUG,它会打印每个Bean被装配的时间,通常慢的元凶是JPA EntityManagerFactoryRedisConnectionFactory

Q3:团队不想用Testcontainers,因为Docker不普及,是否有替代方案? A:如果强行用H2,需注意模式兼容,比如在H2开启MySQL模式:

spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE

但终究有风险,我更建议团队统一使用嵌入式MariaDB(比H2更接近MySQL)。

Q4:为什么我的@MockBean在测试中失效,Service还是调用了真实方法? A:大概率是你在同一个测试类里混用了@MockBean@SpyBean,或者你在@BeforeEach中重新init()了Service,注意:@MockBean必须在Spring上下文刷新前注入,不要在测试方法内部手动赋值覆盖。

Q5:测试中的随机端口如何被客户端代码获取到? A:对于WebEnvironment.RANDOM_PORT,注入@LocalServerPort int port字段,若测试里需要构造完整URL,建议使用URI对象而非字符串拼接。


一个可靠的Spring Boot测试套件,应当是团队代码变更的安全网,而不是CI流水线上的冗余噪音,从单一职责的@WebMvcTest到逼近真实的Testcontainers集成测试,你需要在速度可信度之间找到平衡点,希望本文的三个案例,能让你下次提交代码时,内心多一分从容,少一分“人肉测试”的焦虑。

抱歉,评论功能暂时关闭!