如何在NUnit数据库测试中用Transaction Scope回滚多数据库连接
解决方案
方案1:为每个数据库单独管理本地事务(推荐用于测试数据清理)
.NET Core不支持依赖MSDTC的分布式TransactionScope,但可以给每个数据库的DbContext单独开启本地事务,测试完成后统一回滚,实现数据清理。这种方式不需要分布式事务,完全符合测试场景的需求。
代码示例
private DbContext1 _dbContext1; private DbContext2 _dbContext2; private DbContext3 _dbContext3; private IDbContextTransaction _tran1; private IDbContextTransaction _tran2; private IDbContextTransaction _tran3; [SetUp] public void BeforeTest() { // 初始化各数据库上下文并开启事务 _dbContext1 = new DbContext1(); _tran1 = _dbContext1.Database.BeginTransaction(); _dbContext2 = new DbContext2(); _tran2 = _dbContext2.Database.BeginTransaction(); _dbContext3 = new DbContext3(); _tran3 = _dbContext3.Database.BeginTransaction(); } [TearDown] public void AfterTest() { // 按顺序回滚并释放资源 _tran1.Rollback(); _tran1.Dispose(); _dbContext1.Dispose(); _tran2.Rollback(); _tran2.Dispose(); _dbContext2.Dispose(); _tran3.Rollback(); _tran3.Dispose(); _dbContext3.Dispose(); }
说明
- 每个数据库的操作都在各自的本地事务中执行,测试中调用
SaveChanges()只会将数据写入事务,不会提交到数据库。 TearDown阶段统一回滚所有事务,确保三个数据库的测试数据被彻底清理。- 这种方式不依赖分布式事务,完全适配.NET Core环境,同时.NET Framework 4.7项目也可以兼容使用。
方案2:测试前重置数据库(适合复杂场景)
如果测试逻辑涉及无法通过事务回滚的操作(比如触发了自动提交的存储过程、第三方数据同步),可以在SetUp阶段直接重置数据库:
- 执行预定义的清理脚本,删除测试生成的数据。
- 恢复测试专用的数据库备份到初始状态。
代码示例(执行清理脚本)
[SetUp] public void BeforeTest() { // 清理数据库1的测试数据 using var db1 = new DbContext1(); db1.Database.ExecuteSqlRaw("DELETE FROM TestTable1 WHERE IsTestData = 1"); // 清理数据库2的测试数据 using var db2 = new DbContext2(); db2.Database.ExecuteSqlRaw("DELETE FROM TestTable2 WHERE IsTestData = 1"); // 清理数据库3的测试数据 using var db3 = new DbContext3(); db3.Database.ExecuteSqlRaw("DELETE FROM TestTable3 WHERE IsTestData = 1"); }
说明
- 需要给测试数据标记特殊标识(比如
IsTestData字段),避免误删生产数据。 - 相比事务回滚,这种方式性能稍差,但兼容性更强,适合所有.NET环境。
方案3:跨库操作原子性保障(若需事务一致性)
如果测试场景要求三个数据库的操作必须原子执行(要么全部成功,要么全部失败),可以实现简化版的补偿事务(Saga模式):
- 按顺序执行各数据库的操作,记录每个操作的状态。
- 若某一步失败,反向执行之前所有操作的回滚逻辑(比如删除已插入的数据、恢复修改前的状态)。
代码示例(简化版补偿逻辑)
[Test] public void CrossDbOperationTest() { bool db1Success = false; bool db2Success = false; try { // 操作数据库1 using var db1 = new DbContext1(); db1.TestEntities.Add(new TestEntity { IsTestData = true }); db1.SaveChanges(); db1Success = true; // 操作数据库2 using var db2 = new DbContext2(); db2.OtherEntities.Add(new OtherEntity { IsTestData = true }); db2.SaveChanges(); db2Success = true; // 操作数据库3并验证 using var db3 = new DbContext3(); db3.ValidationEntities.Add(new ValidationEntity { IsTestData = true }); db3.SaveChanges(); // 执行验证逻辑 // ... } catch { // 补偿回滚 if (db2Success) { using var db2 = new DbContext2(); db2.Database.ExecuteSqlRaw("DELETE FROM OtherEntities WHERE IsTestData = 1"); } if (db1Success) { using var db1 = new DbContext1(); db1.Database.ExecuteSqlRaw("DELETE FROM TestEntities WHERE IsTestData = 1"); } throw; } finally { // 清理所有测试数据 using var db1 = new DbContext1(); db1.Database.ExecuteSqlRaw("DELETE FROM TestEntities WHERE IsTestData = 1"); using var db2 = new DbContext2(); db2.Database.ExecuteSqlRaw("DELETE FROM OtherEntities WHERE IsTestData = 1"); using var db3 = new DbContext3(); db3.Database.ExecuteSqlRaw("DELETE FROM ValidationEntities WHERE IsTestData = 1"); } }
说明
- 这种方式需要手动编写回滚逻辑,适合必须保证跨库操作一致性的测试场景。
- 最终的
finally块仍需执行统一清理,确保测试数据被彻底清除。
内容的提问来源于stack exchange,提问作者Vinny K
相关产品推荐
相关产品推荐

