ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

集成测试中如何构造可重复数据——初始化脚本、清理策略与 CI 流水线

集成测试中如何构造可重复数据——初始化脚本、清理策略与 CI 流水线 文章目录每日一句正能量前言1. 背景与问题2. 环境与数据3. 复现过程3.1 复现测试顺序依赖3.2 复现自增 ID 依赖3.3 复现 CI 并行冲突4. 方案实施4.1 Schema 初始化只维护一套迁移脚本4.2 Fixture 应该最小化4.3 SQL Fixture4.4 Sql 清理4.5 事务回滚清理4.6 事务测试不要自动回滚4.7 回滚验证4.8 DELETE 还是 TRUNCATE4.9 不要依赖自增 ID4.10 唯一测试命名4.11 UUID 什么时候适合4.12 MyBatis Fixture4.13 JPA Test Data Builder4.14 JPA 一级缓存会影响断言4.15 清理策略不要和测试事务打架4.16 并行测试独立 Schema4.17 每测试类独立容器4.18 容器复用要小心状态污染4.19 测试数据工厂不要依赖当前时间4.20 事务边界中的 Fixture4.21 并发测试准备数据4.22 清理顺序4.23 初始化脚本与 Fixture 要分工4.24 测试基类4.25 CI 流水线4.26 测试数据可重复性的验收标准4.27 数据准备与清理时序5. 结果对比落地前落地后6. 风险与复盘6.1 不要追求“百分百生产数据仿真”6.2 TRUNCATE 不是万能清理方案6.3 测试自动回滚会掩盖真实提交6.4 并行测试最容易暴露隐藏依赖6.5 Fixture 也要版本管理6.6 不要使用真实敏感数据6.7 时间和随机数要可控制结语每日一句正能量恐惧本身往往比我们恐惧的事物更具破坏力。恐惧常将模糊的威胁放大为不可逾越的巨兽让我们在事情发生前就耗尽心力。破解之道在于正视并拆解恐惧将它从模糊的阴影变为清晰、可应对的具体事物。前言数据库集成测试最让研发团队头疼的往往不是“怎么断言”而是“怎么保证每次跑出来都一样”。一个测试今天通过、明天失败最常见的原因并不是代码随机而是测试数据不稳定上一个用例留下了脏数据 自增 ID 和预期不一致 CI 并行执行时两个用例撞了唯一键 测试依赖执行顺序 本地库和 CI 库初始化状态不同 事务自动回滚掩盖了真实提交行为这类问题会迅速摧毁团队对自动化测试的信任。只要大家开始觉得“数据库测试偶尔红是正常的”CI 就失去了质量门禁意义。因此集成测试数据的核心目标不是“造得多真实”而是做到四件事可重复 可隔离 可清理 可并行本文以 Spring Boot Testcontainers Flyway 为基础给出一套可以落地到研发团队的测试数据管理方案并分别说明 JDBC、MyBatis、JPA/Hibernate 下的 Fixture、清理策略、异常与事务边界。1. 背景与问题先看一个很典型的测试TestvoidshouldCreateOrder(){orderService.create(O-1001,newBigDecimal(100));assertThat(orderRepository.findByOrderNo(O-1001)).isPresent();}单独运行没问题。但整个测试类一起跑时可能失败Duplicate entry O-1001 for key uk_order_no原因很简单另一个测试已经插入过 O-1001。很多团队会临时改成StringorderNoUUID.randomUUID().toString();这样虽然绕过了唯一键冲突却带来另一个问题测试数据不可读 失败难复现 日志难定位真正应该解决的是用例之间不能共享不可控状态。2. 环境与数据示例环境JDK 21 Spring Boot 3.3 JUnit 5 Testcontainers MySQL 8.0 PostgreSQL 15 Flyway Spring JDBC MyBatis 3.x Hibernate 6 / JPA测试表CREATETABLEorders(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_noVARCHAR(64)NOTNULL,user_idBIGINTNOTNULL,amountDECIMAL(18,2)NOTNULL,statusVARCHAR(32)NOTNULL,created_atTIMESTAMP(6)NOTNULLDEFAULTCURRENT_TIMESTAMP(6),UNIQUEKEYuk_order_no(order_no),KEYidx_user_status(user_id,status));流水表CREATETABLEorder_ledger(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_noVARCHAR(64)NOTNULL,amountDECIMAL(18,2)NOTNULL,biz_typeVARCHAR(32)NOTNULL,UNIQUEKEYuk_order_biz(order_no,biz_type));测试 Schema 应由生产 migration 初始化而不是单独维护一份简化版 DDL。3. 复现过程3.1 复现测试顺序依赖测试 ATestvoidcreateOrder(){repository.insert(O-1001,newBigDecimal(100));}测试 BTestvoidduplicateShouldFail(){repository.insert(O-1001,newBigDecimal(100));assertThatThrownBy(()-repository.insert(O-1001,newBigDecimal(200))).isInstanceOf(DuplicateKeyException.class);}如果 A 先执行B 的第一条插入就已经失败。这说明 B 实际依赖数据库开始时没有 O-1001。这个前置条件应该由 B 自己保证而不是靠测试顺序。3.2 复现自增 ID 依赖错误测试repository.insert(...);Orderorderrepository.findById(1L);assertThat(order).isNotNull();如果其他测试已经插过数据ID 未必是 1。测试应该依赖业务唯一键而不是数据库自增序列的偶然状态。3.3 复现 CI 并行冲突两个测试类同时运行Class A - O-TEST-1 Class B - O-TEST-1共享同一个数据库时会撞唯一键。本地串行运行却一直成功。这就是典型本地绿CI 红。4. 方案实施4.1 Schema 初始化只维护一套迁移脚本TestcontainersTestcontainersSpringBootTestclassIntegrationTestBase{ContainerstaticMySQLContainer?mysqlnewMySQLContainer(mysql:8.0).withDatabaseName(testdb).withUsername(test).withPassword(test);DynamicPropertySourcestaticvoidregister(DynamicPropertyRegistryregistry){registry.add(spring.datasource.url,mysql::getJdbcUrl);registry.add(spring.datasource.username,mysql::getUsername);registry.add(spring.datasource.password,mysql::getPassword);}}Spring 启动后Flyway 自动执行这样测试环境和生产使用同一套V001 V002 V003 ...迁移脚本。4.2 Fixture 应该最小化不要每个测试启动前灌入几万条“仿真数据”。一个订单测试通常只需要1 个用户 1~3 个订单 必要的关联数据例如publicfinalclassOrderFixture{publicstaticOrderCommandorder(StringorderNo){returnnewOrderCommand(orderNo,101L,newBigDecimal(100.00));}}测试OrderCommandcmdOrderFixture.order(T-ORDER-001);Fixture 的价值是语义明确 字段集中 修改方便4.3 SQL Fixture有些复杂关联数据用 SQL 更清晰。INSERTINTOusers(id,user_name)VALUES(101,test-user);INSERTINTOorders(order_no,user_id,amount,status)VALUES(T-ORDER-001,101,100.00,CREATED);JUnitSql(scripts/fixtures/order_created.sql,executionPhaseSql.ExecutionPhase.BEFORE_TEST_METHOD)这种方式适合固定数据库状态4.4 Sql 清理可以配置Sql(scripts/fixtures/cleanup.sql,executionPhaseSql.ExecutionPhase.AFTER_TEST_METHOD)清理脚本DELETEFROMorder_ledger;DELETEFROMorders;DELETEFROMusers;注意外键顺序。4.5 事务回滚清理最简单TransactionalTestvoidshouldCreateOrder(){...}Spring Test 在用例结束后回滚。优点快 无需清理 SQL 测试隔离好但它不能覆盖所有场景。4.6 事务测试不要自动回滚如果测试目标就是真实 commit就不能让测试框架自动 rollback。例如TestvoidserviceShouldCommitOrder(){service.createOrder(...);Orderorderrepository.findByOrderNo(T-COMMIT-001);assertThat(order).isNotNull();}Service 自己的Transactional真正完成提交。测试方法本身不加事务。4.7 回滚验证TestvoidledgerFailureShouldRollbackOrder(){assertThatThrownBy(()-service.createOrderWithLedger(T-ROLLBACK-001)).isInstanceOf(DataAccessException.class);assertThat(repository.exists(T-ROLLBACK-001)).isFalse();}这里验证的就是真实业务事务而不是测试框架帮你清理。4.8 DELETE 还是 TRUNCATEDELETEDELETEFROMorder_ledger;DELETEFROMorders;优点事务友好 可按条件删除TRUNCATETRUNCATETABLEorder_ledger;TRUNCATETABLEorders;优点大表清理快但要注意外键 权限 自增序列 数据库事务语义4.9 不要依赖自增 ID推荐业务唯一键稳定 ID 由数据库生成 测试按业务键查询例如repository.findByOrderNo(T-ORDER-001);而不是findById(1L);4.10 唯一测试命名CI 并行时可以给每个测试类一个前缀。StringorderNoOrderServiceTest-001;或者StringprefixgetClass().getSimpleName();组合OrderServiceTest-O001 OrderMapperTest-O001这样既可读又降低冲突。4.11 UUID 什么时候适合UUID 并不是不能用。适合测试只关心唯一性 不需要人工定位但核心业务用例最好保留可读命名。例如T-DUPLICATE-001 T-ROLLBACK-001 T-CONCURRENT-001日志一眼就知道是哪类测试。4.12 MyBatis FixtureMapperinsertidinsertINSERT INTO orders( order_no, user_id, amount, status ) VALUES( #{orderNo}, #{userId}, #{amount}, #{status} )/insert测试里直接调用BeforeEachvoidprepare(){orderMapper.insert(newOrder(null,T-MYBATIS-001,101L,newBigDecimal(100),CREATED));}不要用生产 Service 做 Fixture。否则测试 Repository 却依赖 Service层次会变乱。4.13 JPA Test Data BuilderpublicclassOrderEntityBuilder{privateStringorderNoT-JPA-DEFAULT;privateBigDecimalamountnewBigDecimal(100.00);publicOrderEntityBuilderorderNo(Stringvalue){this.orderNovalue;returnthis;}publicOrderEntitybuild(){returnnewOrderEntity(orderNo,amount,CREATED);}}测试OrderEntityentitynewOrderEntityBuilder().orderNo(T-JPA-001).build();repository.saveAndFlush(entity);Builder 可以集中处理默认必填字段避免每个测试重复构造。4.14 JPA 一级缓存会影响断言例如repository.save(entity);service.updateByNativeSql(...);assertThat(entity.getStatus()).isEqualTo(PAID);这可能失败因为EntityManager 中还是旧对象。测试原生 SQL 或触发器时需要entityManager.flush();entityManager.clear();再重新查询。这是 ORM 集成测试中很重要的数据边界。4.15 清理策略不要和测试事务打架例如测试方法加Transactional然后AfterEach又执行TRUNCATE不同事务/连接下可能产生锁或可见性问题。团队应该明确这一类测试由 rollback 清理 另一类测试由 cleanup SQL 清理不要混用得太随意。4.16 并行测试独立 SchemaPostgreSQL 可以为每个测试任务创建test_01 test_02 test_03MySQL 可以用独立数据库testdb_01 testdb_02CI JobJOB_INDEX1生成testdb_1这样不同 Job 不共享数据。4.17 每测试类独立容器隔离最强ContainerstaticPostgreSQLContainer?pgnewPostgreSQLContainer(postgres:16);每个测试类拥有自己数据库状态。代价是启动时间 CPU 内存可以用于关键测试套件。4.18 容器复用要小心状态污染本地为了加速可以复用 Testcontainers。但如果容器长期复用就必须确保Schema 清理 测试数据清理 migration 状态都能重新确定。否则“复用”会再次引入状态依赖。4.19 测试数据工厂不要依赖当前时间错误createdAtInstant.now();断言可能因为时区、执行速度产生不稳定。更稳妥InstantfixedInstant.parse(2026-01-01T00:00:00Z);业务确实需要当前时间时可注入Clock4.20 事务边界中的 Fixture如果 Fixture 数据和待测业务必须处在不同事务必须显式安排。例如先提交库存初始值 再开启业务事务测试锁行为如果 Fixture 仍在未提交测试事务里另一个线程可能根本看不到。这在并发测试中非常常见。4.21 并发测试准备数据先inventoryRepository.init(1001L,10);确保提交。再启动 20 个线程。不要在TransactionalTest方法里准备库存后立即开多线程。因为子线程用的是其他连接看不到未提交的数据。4.22 清理顺序有关联表时先子表 后父表例如DELETEFROMorder_ledger;DELETEFROMorders;DELETEFROMusers;或者测试 Schema 提供专用 teardown 脚本统一维护。4.23 初始化脚本与 Fixture 要分工迁移脚本负责 SchemaFixture负责测试数据不要在 Flyway migration 里塞大量测试专用数据。否则生产也会得到这些数据。4.24 测试基类可以统一封装publicabstractclassDbIntegrationTest{AutowiredJdbcTemplatejdbcTemplate;BeforeEachvoidclean(){jdbcTemplate.update(DELETE FROM order_ledger);jdbcTemplate.update(DELETE FROM orders);}}但要避免基类越来越大。更好的做法是按领域拆 Fixture / Cleaner。4.25 CI 流水线推荐顺序启动数据库容器 - Flyway migration - Repository 测试 - Service 事务测试 - 并发测试 - 销毁环境如果 migration 本身失败后续测试直接停止。4.26 测试数据可重复性的验收标准一个测试至少应满足单独运行通过 全量运行通过 顺序打乱通过 重复运行通过 CI 并行通过 本地和 CI 一致如果其中某一项失败说明数据隔离还不够。4.27 数据准备与清理时序推荐形成团队约定BeforeEach - 清理 - Fixture Test - 执行业务 - 断言 AfterEach - 清理或者测试事务 - 自动回滚但每个测试类型要有明确策略。5. 结果对比落地前测试数据共享数据库 手工插入 依赖执行顺序 偶发唯一键冲突 CI 并行不稳定典型结果本地绿 CI 偶发红 重新跑又绿开发人员最后选择点 Retry。落地后SchemaTestcontainers FlywayFixture最小、可读、每测自包含清理rollback / DELETE / TRUNCATE 按测试类型选择并行唯一命名 独立 Schema 或独立容器结果测试顺序无关 重复执行一致 失败可稳定复现 CI 不再依赖“运气”6. 风险与复盘6.1 不要追求“百分百生产数据仿真”集成测试核心是验证边界。数据规模测试和性能测试应该单独做。普通测试用最小 Fixture 更可靠。6.2 TRUNCATE 不是万能清理方案存在外键 权限 事务差异时可能不适用。先理解数据库行为再选。6.3 测试自动回滚会掩盖真实提交普通 CRUD 测试可以用。事务、异步、并发、commit 异常测试必须显式提交。6.4 并行测试最容易暴露隐藏依赖不要为了让 CI 绿而关闭并行。并行失败往往说明共享数据 共享全局 ID 共享缓存存在问题。6.5 Fixture 也要版本管理表结构变化后Fixture Builder SQL Fixture 清理脚本都要同步升级。6.6 不要使用真实敏感数据CI 环境应该使用合成数据 脱敏数据不能直接复制生产用户数据。6.7 时间和随机数要可控制需要可重复时应固定 Clock 固定 Random seed避免测试因为系统时间和随机值漂移。结语集成测试数据治理的目标不是让测试数据库“越来越像生产”而是让每个测试都拥有一个清晰、独立、可恢复的初始状态。一套成熟方案通常是真实数据库容器 生产 Schema migration 最小 Fixture 明确清理策略 独立事务边界 CI 并行隔离可以把核心原则总结成一句话测试必须自己准备前置条件 自己验证结果 也自己负责清理现场。当测试结果不再依赖执行顺序、机器状态和上一个用例留下的数据时数据库集成测试才真正具备“可重复”这个最重要的工程属性。转载自https://blog.csdn.net/u014727709/article/details/165243548欢迎 点赞✍评论⭐收藏欢迎指正
返回列表