当前位置: 首页 > news >正文

还在 @AfterEach 里手动 deleteAll()?你早就该试试这个测试数据清理 Starter 了

图片

编写集成测试时,我们都面临一个共同的挑战:如何保证每个测试用例都在一个干净、隔离的数据库环境中运行?
传统的做法是在每个测试类的 @AfterEach (或 @After) 方法中,手动调用 xxxRepository.deleteAll() 来清理数据。这种方式的弊端显而易见:

  • • 代码冗余: 每个测试类都需要重复编写清理逻辑。

  • • 顺序依赖: 在处理有外键关联的表时,你必须小心翼翼地按照正确的反向顺序来删除数据,否则就会触发外键约束异常。

  • • 效率低下: DELETE 操作会逐行删除,并记录 binlog,对于大量数据,速度很慢。

本文将带你从 0 到 1,构建一个基于注解的、对业务代码零侵入的集成测试数据清理 Starter。你只需在测试类上添加一个 @CleanDatabase 注解,它就能在每个测试方法执行后,自动、高效地将数据库恢复到“出厂设置”。

1. 项目设计与核心思路

我们的 test-data-cleaner-starter 目标如下:

  1. 1. 注解驱动: 提供 @CleanDatabase 注解,轻松标记需要数据清理的测试。

  2. 2. 自动执行: 在每个被标记的 @Test 方法执行之后自动触发清理。

  3. 3. 高效清理: 采用 TRUNCATE TABLE 策略,速度远超 DELETE

  4. 4. 智能识别: 自动识别所有业务表,并能智能地排除掉 Flyway/Liquibase 等迁移工具的系统表。

核心实现机制:Spring TestExecutionListener
Spring TestContext Framework 提供了一个强大的扩展点 TestExecutionListener。它允许我们在测试生命周期的各个阶段(如测试类执行前、测试方法执行后)嵌入自定义逻辑。
我们将创建一个自定义的 TestExecutionListener,它会在 afterTestMethod 这个阶段被触发,检查测试类或方法上是否有 @CleanDatabase 注解,如果有,则执行数据库清理操作。

2. 创建 Starter 项目与核心组件

我们采用 autoconfigure + starter 的双模块结构。

步骤 2.1: 依赖 (autoconfigure 模块)
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency></dependencies>
步骤 2.2: 定义核心注解与清理服务

@CleanDatabase (清理注解):

package com.example.testcleaner.autoconfigure.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CleanDatabase {
}

DatabaseCleaner.java (核心清理服务):

package com.example.testcleaner.autoconfigure.core;import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import java.util.stream.Collectors;public class DatabaseCleaner {private final JdbcTemplate jdbcTemplate;public DatabaseCleaner(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}public void clean() {// 1. 关闭外键约束jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0;");// 2. 查找所有业务表并 TRUNCATEList<String> tables = fetchAllTables();tables.forEach(tableName -> {jdbcTemplate.execute("TRUNCATE TABLE `" + tableName + "`;");});// 3. 重新开启外键约束jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1;");}private List<String> fetchAllTables() {return jdbcTemplate.queryForList("SHOW TABLES", String.class).stream()// 智能排除 Flyway 和 Liquibase 的系统表.filter(tableName -> !tableName.startsWith("flyway_schema_history")).filter(tableName -> !tableName.startsWith("databasechangelog")).collect(Collectors.toList());}
}
步骤 2.3: 实现 TestExecutionListener

这是连接注解和清理服务的桥梁。

package com.example.testcleaner.autoconfigure.listener;import com.example.testcleaner.autoconfigure.annotation.CleanDatabase;
import com.example.testcleaner.autoconfigure.core.DatabaseCleaner;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;public class CleanDatabaseTestExecutionListener extends AbstractTestExecutionListener {// 在所有 listener 中,我们希望它最后执行@Overridepublic int getOrder() {return Integer.MAX_VALUE;}@Overridepublic void afterTestMethod(TestContext testContext) throws Exception {// 检查方法或类上是否有 @CleanDatabase 注解if (testContext.getTestMethod().isAnnotationPresent(CleanDatabase.class) ||testContext.getTestClass().isAnnotationPresent(CleanDatabase.class)) {// 从 Spring 容器中获取我们的清理服务 BeanDatabaseCleaner cleaner = testContext.getApplicationContext().getBean(DatabaseCleaner.class);cleaner.clean();}}
}

3. 自动装配的魔法 (DataCleanerAutoConfiguration)

package com.example.testcleaner.autoconfigure;import com.example.testcleaner.autoconfigure.core.DatabaseCleaner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;@Configuration
@ConditionalOnProperty(prefix = "test.data-cleaner", name = "enabled", havingValue = "true", matchIfMissing = true)
public class DataCleanerAutoConfiguration {@Beanpublic DatabaseCleaner databaseCleaner(JdbcTemplate jdbcTemplate) {return new DatabaseCleaner(jdbcTemplate);}
}
步骤 3.1: 注册 TestExecutionListener (关键一步)

为了让 Spring Test 框架能自动发现我们的 Listener,我们需要在 autoconfigure 模块的 resources/META-INF/spring.factories 文件中进行注册。

resources/META-INF/spring.factories

org.springframework.test.context.TestExecutionListener=\
com.example.testcleaner.autoconfigure.listener.CleanDatabaseTestExecutionListener

4. 如何使用我们的 Starter

步骤 4.1: 引入依赖
在你的业务项目 pom.xml 中,确保在 test scope 下添加依赖:

<dependency><groupId>com.example</groupId><artifactId>test-data-cleaner-spring-boot-starter</artifactId><version>1.0.0</version><scope>test</scope>
</dependency>

步骤 4.2: 在测试类中使用注解
现在,你的集成测试可以变得无比清爽。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
@CleanDatabase // <-- 在类上添加注解,对所有测试方法生效
public class OrderServiceIntegrationTest {@Autowiredprivate OrderService orderService;@Autowiredprivate OrderRepository orderRepository;@Testvoid shouldCreateOrderSuccessfully() {// Given: 一个干净的数据库// WhenOrder newOrder = orderService.createOrder(...);// Thenassert orderRepository.findById(newOrder.getId()).isPresent();} // <-- 在此方法执行完毕后,Starter 会自动 TRUNCATE 所有表@Testvoid shouldFindNoOrdersInitially() {// Given: 一个干净的数据库 (因为上一个测试的数据已被清理)// WhenList<Order> orders = orderRepository.findAll();// Thenassert orders.isEmpty();}
}

总结

通过自定义一个 Spring Boot Starter 和巧妙地利用 TestExecutionListener,我们成功地将繁琐、易错的测试数据清理逻辑,封装成了一个声明式的 @CleanDatabase 注解。

http://www.xdnf.cn/news/1449559.html

相关文章:

  • leetcode110. 平衡二叉树
  • mysql常见面试题
  • [光学原理与应用-376]:ZEMAX - 优化 - 概述
  • 代码随想录算法训练营第四天|链表part02
  • SQLint3 模块如何使用
  • PostgreSQL 技术峰会哈尔滨站活动回顾|深度参与 IvorySQL 开源社区建设的实践与思考
  • 农业XR数字融合工作站,赋能农业专业实践学习
  • 刻意练习实践说明使用手册
  • 正则表达式的使用
  • Java jar 如何防止被反编译?代码写的太烂,害怕被人发现
  • TDD测试驱动开发+Python案例解析
  • 在linux下使用MySQL常用的命令集合
  • 通义实验室发布AgentScope 1.0新一代智能体开发框架
  • 嵌入式第四十二天(数据库,网页设计)
  • Spring Boot集成Kafka常见业务场景最佳实践实战指南
  • Java全栈工程师的面试实战:从基础到复杂问题的完整解析
  • 安卓APP备案的三要素包名,公钥,签名md5值详细获取方法-优雅草卓伊凡
  • 鹧鸪云软件:光伏施工管理一目了然,进度尽在掌握
  • 涉私数据安全与可控匿名化利用机制研究(下)
  • Selenium WebUI 自动化“避坑”指南——从常用 API 到 10 大高频问题
  • 本地化AI问答:告别云端依赖,用ChromaDB + HuggingFace Transformers 搭建离线RAG检索系统
  • 科技信息差(9.3)
  • uni app 的app端 写入运行日志到指定文件夹。
  • Linux学习:生产者消费者模型
  • 开源 C++ QT Widget 开发(十一)进程间通信--Windows 窗口通信
  • AI 大模型 “内卷” 升级:从参数竞赛到落地实用,行业正在发生哪些关键转变?
  • 2025年经济学专业女性职业发展证书选择与分析
  • SCN随机配置网络时间序列预测Matlab实现
  • @Resource与@Autowired的区别
  • 数据结构——顺序表和单向链表(2)