2

Scenario

I have an API on ASP.NET Core 2.0 that integrates with MS SQL database using EF Core. Now I am trying to setup integration/api tests for it using NUnit and TestServer. The issue is that I need to configure every test to be 'isolated' so basically it should clean up (rollback) DB after self. I cannot use compensation transactions to achieve the desired result due to the complexity of DB (lots of legacy stuff to account for, e.g. triggers etc).

SUT API Setup

Here is an example of API I am trying to test:

// 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();
}

The DoWork() method basically looks up an entity given passed parameter and increments its another property. Then simply calls SaveChanges(). The CreateScope() is here a helper method that returns an instance of TransactionScope:

return new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions() { IsolationLevel = IsolationLevel.RepeatableRead },
    TransactionScopeAsyncFlowOption.Enabled
);

Integration Test Setup

[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;
    }
}

Here the Client is an instance of HttpClient created by utilizing Microsoft.AspNetCore.TestHost.TestServer and CreateScope() method is actually the same as in the API. This simple case works fine - the changes made by my SUT API are rollbacked successfully by calling _scope.Dispose() and DB returns to its 'clean' state.

Problem

Now I want to move the logic related to create/rollback of scope outside of my test method and put it in SetUp/TearDown so all my tests are automatically handled.

[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());
}

But it does NOT work (I can see modifications in DB after test run) for some reason and I cannot figure it out.

Why? What am I missing?

Note: both tests versions pass successfully.

Yuriy Ivaskevych
  • 956
  • 8
  • 18
  • 1
    please try removing `async` in your setup and teardown, you don't need it anyway. This could be the cause of the problem, need to confirm. – Khanh TO Mar 13 '20 at 09:16
  • my guess is that even you use `TransactionScopeAsyncFlowOption.Enabled`, but since the `_scope` is created and disposed in your `async` setup and teardown, it's created in `another thread context`, the scope inside your SUT does not participate in this context, so it has no effect – Khanh TO Mar 13 '20 at 09:20
  • @KhanhTO but I need the `async` in `SetUp` at least because I'll need to put some data in DB before test run (the same applies to `OneTimeSetUp`/teardown and I've planned to have one more `TransactionScope` at that level as well. – Yuriy Ivaskevych Mar 13 '20 at 09:41
  • I'm not sure if it's a good idea to put data in DB in `[Setup]`, you may run this multiple times (one for each test), but since you have rollback already, this is unnessary. Maybe it makes more sense to put data only in `OneTimeSetUp`, in `OneTimeSetUp`, you don't need to rollback so you can use `async` there just fine – Khanh TO Mar 13 '20 at 09:47
  • @KhanhTO Hmm, actually it does work with `void` SetUp/TearDown, just confirmed. I guess I can go with such config and then manually cleanup preloaded data in `OneTimeSetUp`/teardown../ – Yuriy Ivaskevych Mar 13 '20 at 09:49
  • 1
    @KhanhTO yeah, could you please post it as an answer so I can accept it? It makes sence, I think I'll stick to your solution now, thanks! – Yuriy Ivaskevych Mar 13 '20 at 09:50
  • The only thing that seems weird to me, i heard that .NET Core removed synchronization context, but `TransactionScopeAsyncFlowOption.Enabled` still works, my previous assumption about removing `async` also relies on the synchronization context, but i will check that later – Khanh TO Mar 13 '20 at 09:56

2 Answers2

2

The possible solution is removing async from [SetUp] and [TearDown].

Explanation:

When TransactionScopeAsyncFlowOption.Enabled is used, the transaction scope should flow across thread continuations, but since _scope is created and disposed in the async setup and teardown, it's created and disposed in other thread contexts (not the same thread continuation). The scope inside the SUT does not participate in this context, so it has no effect

Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • This worked fine in .NET Framework - only .NET Core appears to have problems with this. It may well be a bug. – ajbeaven Mar 18 '21 at 20:41
1

You're missing the multiple threads. TransactionScope uses thread local storage. So you should be constructing, using and disposing of it all on one thread. Quoting the 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.

So if you want to use TransactionScope in a thread-safe manner you need to use DependentTransaction. See here for an example on how to do so safely.

Edit

You can also use TransactionScopeAsyncFlowOption.Enabled when constructing the scope, which will prevent TLS and allow scope to flow through async/await calls.

Be aware the default is TransactionScopeAsyncFlowOption.Suppress.

Zer0
  • 7,191
  • 1
  • 20
  • 34
  • Hmm, but I already use `TransactionScopeAsyncFlowOption.Enabled` as can be seen from `CreateScope()`. Also I'm wondering whether `SingleThreadedAttribute` applies to `SetUp`/`TearDown` as according to [docs](https://github.com/nunit/docs/wiki/SingleThreaded-Attribute) it does for `OneTimeSetUp` and teardown... The problem with `DependentTransaction` however is that I don't really want to 'pollute' my test methods and hence tried to put it in setup/teardown. Is there a way I can possibly rearchitecture tests to achieve the result without obviously cluttering the tests? – Yuriy Ivaskevych Mar 13 '20 at 08:00