TransactionScope не выполняет откат в тесте TearDown для тестирования интеграции с использованием NUnit и TestServer. - PullRequest
1 голос
/ 12 марта 2020

Сценарий

У меня есть API на ASP. NET Core 2.0, который интегрируется с базой данных MS SQL с использованием EF Core. Сейчас я пытаюсь настроить интеграционные / API-тесты для него, используя NUnit и TestServer. Проблема в том, что мне нужно настроить каждый тест как «изолированный», так что в основном он должен очищать (откатывать) БД после самостоятельной работы. Я не могу использовать компенсационные транзакции для достижения желаемого результата из-за сложности БД (множество унаследованных вещей, например, триггеров и т. Д. c).

Настройка API SUT

Здесь пример 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() - это вспомогательный метод, который возвращает экземпляр 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 является экземпляром HttpClient созданный с использованием методов Microsoft.AspNetCore.TestHost.TestServer и CreateScope() фактически такой же, как в API. Этот простой случай работает нормально - изменения, сделанные моим SUT API, успешно откатываются путем вызова _scope.Dispose() и DB возвращается в свое «чистое» состояние.

Проблема

Теперь я хочу переместить logi c относится к созданию / откату области действия вне моего метода тестирования и помещает его в 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());
}

Но это НЕ работает (я вижу изменения в БД после пробного запуска) по какой то причине и не могу разобраться.

Почему? Чего мне не хватает?

Примечание: обе версии теста пройдены успешно.

Ответы [ 2 ]

1 голос
/ 13 марта 2020

Возможным решением является удаление async из [SetUp] и [TearDown].

Объяснение:

При использовании TransactionScopeAsyncFlowOption.Enabled область действия транзакции должен проходить через продолжения потока, но поскольку _scope создается и располагается в настройке async и демонтаже, он создается и располагается в других контекстах потока (не то же самое продолжение потока). scope внутри SUT не участвует в этом контексте, поэтому он не имеет никакого эффекта

1 голос
/ 12 марта 2020

Вам не хватает нескольких потоков. TransactionScope использует локальное хранилище потока. Таким образом, вы должны конструировать, использовать и размещать все это в одном потоке. Цитирование документации :

Вы также должны использовать классы TransactionScope и DependentTransaction для приложений, которые требуют использования одной и той же транзакции для нескольких вызовов функций или вызовов нескольких потоков.

Поэтому, если вы хотите использовать TransactionScope в поточно-ориентированном режиме, вам нужно использовать DependentTransaction. См. здесь для примера того, как сделать это безопасно.

Редактировать

Вы также можете использовать TransactionScopeAsyncFlowOption.Enabled при построении области, которая будет предотвращать TLS и позволять области видимости проходить через асинхронные / ожидающие вызовы.

Имейте в виду, что по умолчанию установлено значение TransactionScopeAsyncFlowOption.Suppress.

...