TransactionScope 不会在使用 NUnit 和 TestServer 进行集成测试的每个测试 TearDown 中回滚

TransactionScope doesn't rollback in per-test TearDown for integration testing using NUnit and TestServer

场景

我在 ASP.NET Core 2.0 上有一个 API,它使用 EF Core 与 MS SQL 数据库集成。现在我正在尝试使用 NUnit 和 TestServer 为它设置 integration/api 测试。问题是我需要将每个测试配置为 'isolated' 所以基本上它应该在自我之后清理(回滚)数据库。由于数据库的复杂性(需要考虑很多遗留问题,例如触发器等),我无法使用补偿交易来达到预期的结果。

SUT API 设置

这是我正在尝试测试的 API 示例:

// GET api/values
[HttpGet]
public async Task<IActionResult> Get(string dataName1, string dataName2)
{
    using (var scope = CreateScope())
    {
        await _service.DoWork(dataName1);
        await _service.DoWork(dataName2);
        scope.Complete();
    }
    return Ok();
}

DoWork() 方法基本上查找给定传递参数的实体并递增其另一个 属性。然后简单地调用 SaveChanges()CreateScope() 在这里是一个辅助方法 returns TransactionScope:

的一个实例
return new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions() { IsolationLevel = IsolationLevel.RepeatableRead },
    TransactionScopeAsyncFlowOption.Enabled
);

集成测试设置

[TestFixture]
[SingleThreaded]
[NonParallelizable]
public class TestTest
{
    private TransactionScope _scope;

    [Test]
    public async Task Test11()
    {
        _scope = CreateScope();
        var result = await Client.GetAsync("api/values?dataName1=Name1&dataName2=Name2");
        Assert.DoesNotThrow(() => result.EnsureSuccessStatusCode());

        _scope.Dispose();
        _scope = null;
    }
}

这里的Client是利用Microsoft.AspNetCore.TestHost.TestServer创建的HttpClient实例,CreateScope()方法其实和API一样。 这个简单的案例工作正常——我的 SUT API 所做的更改通过调用 _scope.Dispose() 和 DB returns 成功回滚到其 'clean' 状态。

问题

现在我想将与范围 create/rollback 相关的逻辑移到我的测试方法之外,并将其放在 SetUp/TearDown 中,以便自动处理我的所有测试。

[SetUp]
public async Task SetupTest()
{
    _scope = TransactionHelper.CreateScope();
}

[TearDown]
public async Task TeardownTest()
{
    _scope.Dispose();
    _scope = null;
}

[Test]
public async Task Test11()
{
    var result = await OneTimeTestFixtureStartup.Client.GetAsync("api/values?dataName1=Name1&dataName2=Name2");
    Assert.DoesNotThrow(() => result.EnsureSuccessStatusCode());
}

但由于某种原因,它不起作用(测试后我可以在数据库中看到修改 运行),我无法弄清楚。

为什么?我错过了什么?

注意:两个测试版本都成功通过。

您错过了多线程。 TransactionScope 使用线程本地存储。所以你应该在一个线程上构建、使用和处理它。引用 documentation:

You should also use the TransactionScope and DependentTransaction class for applications that require the use of the same transaction across multiple function calls or multiple thread calls.

因此,如果您想以线程安全的方式使用 TransactionScope,则需要使用 DependentTransaction。有关如何安全执行此操作的示例,请参阅 here

编辑

您还可以在构建范围时使用 TransactionScopeAsyncFlowOption.Enabled,这将阻止 TLS 并允许范围通过 async/await 调用。

请注意默认值为 TransactionScopeAsyncFlowOption.Suppress

可能的解决方案是从 [SetUp][TearDown] 中删除 async

解释:

当使用 TransactionScopeAsyncFlowOption.Enabled 时,事务作用域应该跨线程延续流动,但是由于 _scope 是在 async 设置和拆卸中创建和处理的,它是在 async 中创建和处理的其他线程上下文(不是同一个线程延续)。 SUT里面的scope不参与这个上下文,所以没有作用