Playwright集成测试因事务锁定无法完成问题排查
问题分析与解决方案
核心误区:Playwright测试的跨进程特性
你当前的问题根源在于Playwright是端到端(E2E)测试框架,测试代码与被测系统(SUT)运行在两个独立进程中——测试代码在本地进程执行,而SUT是独立启动的Web服务进程。这意味着:
- 测试进程中创建的DbContext、开启的事务,完全无法被SUT进程中的DbContext共享,因为事务是绑定到数据库连接实例的,跨进程无法传递连接或事务对象。
- 即使你在Startup中配置强制使用同一数据库实例,也只是让SUT进程内的DbContext共享连接,和测试进程的DbContext没有任何关联。
当前代码的具体问题
- DbContext基类的
transaction变量来源不明:构造函数中判断transaction != null,但代码里没有体现这个变量是如何传递到SUT的DbContext实例中的——跨进程下根本无法传递,所以这个逻辑本质上不生效。 - ReadUncommitted隔离级别无法解决锁问题:该级别只是允许读取未提交的数据,但事务内的写操作依然会加排他锁,SUT进程访问被修改的数据时依然会被阻塞。
可行解决方案
方案1:切换为进程内集成测试(放弃Playwright)
如果你的测试目标是验证业务逻辑与数据库的交互,而非前端UI流程,可以改为进程内集成测试:
- 直接在测试代码中实例化服务层/控制器,不启动独立Web服务。
- 此时测试代码与SUT共享同一个进程和依赖注入容器,就可以通过共享事务的方式实现测试数据的隔离与回滚:
- 在测试Scope中开启事务,并将事务实例注册到容器。
- 所有DbContext构造时从容器中获取事务并绑定。
- 测试结束后统一回滚事务。
方案2:适配Playwright的E2E测试数据隔离
如果必须保留Playwright的UI测试,需要换一种数据隔离方式:
- 每个测试使用独立数据库:测试开始前创建全新的测试数据库(基于模板库克隆),测试结束后销毁。这种方式完全隔离,不会有锁问题。
- 测试前后执行SQL脚本:测试开始前运行清理脚本+测试数据插入脚本;测试结束后运行清理脚本(比如删除测试数据、重置序列等)。注意脚本要避免长时间锁表,尽量使用批量操作。
- 启用SQL Server快照隔离:在数据库上开启快照隔离,这样SUT读取数据时不会被测试进程的事务阻塞,但需要注意快照隔离的性能开销和版本存储问题。
针对同进程场景的代码修正(如果切换为进程内测试)
如果改为进程内集成测试,需要修正事务传递逻辑:
- 修改测试的
getContext方法,将事务注册到当前Scope:
public T getContext<T>() where T : DbContext { var context = Scope.ServiceProvider.GetRequiredService<T>(); Contexts.Add(context); // 开启事务后,将事务实例注册到Scope var trans = context.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted); Scope.ServiceProvider.GetRequiredService<ITestTransactionHolder>().SetTransaction(trans); Transactions.Add(trans); return context; }
- 定义一个事务持有者服务:
public interface ITestTransactionHolder { IDbContextTransaction Transaction { get; set; } void SetTransaction(IDbContextTransaction trans); } public class TestTransactionHolder : ITestTransactionHolder { public IDbContextTransaction Transaction { get; set; } public void SetTransaction(IDbContextTransaction trans) => Transaction = trans; }
- 在测试的依赖注入配置中注册该服务为Scoped:
services.AddScoped<ITestTransactionHolder, TestTransactionHolder>();
- 修改DbContext基类,从容器中获取事务:
public DBContextBase(DbContextOptions options, ITestTransactionHolder transactionHolder) : base(options) { if (transactionHolder?.Transaction != null) { Database.UseTransaction(transactionHolder.Transaction.GetDbTransaction()); } }
内容的提问来源于stack exchange,提问作者BWhite
相关产品推荐
相关产品推荐

