等待不在 SpecFlow 异步测试步骤中返回

Await not returning in SpecFlow async test step

我在处理一些异步 SpecFlow 单元测试时遇到问题,其中 await 永远不会 returning。我认为它与 SpecFlow SynchronizationContext 有关,但我不太确定它是如何工作的。

class 我正在编写测试以在 IScheduler 上定期运行代码。此代码等待在外部事件上完成的任务。在单元测试中,IScheduler 是一个 TestScheduler,外部事件任务由一些存根代码模拟。

我能够让它工作的唯一方法是确保从 同步 SpecFlow 测试步骤调用等待和任务完成。

我的假设是,当从 异步 测试步骤调用 await 时,会捕获 SynchronizationContext.Current 并且当任务完成时它会尝试 return在该 SynchronizationContext 上,但它不再有效,因为它是先前的测试步骤。当从 synchronous 测试步骤调用时,我注意到 SynchronizationContext.Current 为空,我认为这就是等待 returns 与任务完成同步的原因(这就是我要测试)

我可以对 await and/or 任务完成做些什么吗(两者都在测试存根代码中)以确保 await 忽略 SynchronizationContext.Current 并始终同步 returns测试?

我正在使用 SpecFlow 3.9.58

我能够使用以下代码重现此问题:

Feature: Await

Scenario: Wait Async, Set Async
    Given we await asynchronously
    When we set asynchronously
    Then await should have completed

Scenario: Wait Sync, Set Sync
    Given we await synchronously
    When we set synchronously
    Then await should have completed

Scenario: Wait Async, Set Sync
    Given we await asynchronously
    When we set synchronously
    Then await should have completed

Scenario: Wait Sync, Set Async
    Given we await synchronously
    When we set asynchronously
    Then await should have completed

Scenario: Wait Sync, Set Async, Delay
    Given we await synchronously
    When we set asynchronously
    And we wait a bit
    Then await should have completed
using Microsoft.Reactive.Testing;
using NUnit.Framework;
using System.Reactive.Concurrency;
using System.Threading;
using System.Threading.Tasks;
using TechTalk.SpecFlow;

namespace Test.SystemTests
{
    [Binding]
    public sealed class AwaitSteps
    {
        private TestScheduler _scheduler = new TestScheduler();
        private TaskCompletionSource _tcs;
        private bool _waiting;

        // The previous Wait() implementation before Zer0's answer
        //private async Task Wait()
        //{
        //    _tcs = new TaskCompletionSource();
        //    _waiting = true;
        //    await _tcs.Task;
        //    _waiting = false;
        //}

        // Updated Wait()/NestedWait() following Zer0's answer
        private async Task NestedWait()
        {
            // Simulate the code in the stub class (can change this)
            _tcs = new TaskCompletionSource();
            await _tcs.Task.ConfigureAwait(false);
        }

        private async Task Wait()
        {
            // Simulate the code in the class under test (prefer not to change this)
            _waiting = true;
            await NestedWait();
            _waiting = false;
        }

        private void WaitOnScheduler()
        {
            _scheduler.Schedule(async () => await Wait());
            _scheduler.AdvanceBy(1);
        }

        private void Set() => _tcs.TrySetResult();

        [Given(@"we await asynchronously")]
        public async Task GivenWeAwaitAsynchronously()
        {
            await Task.CompletedTask; // Just to make the step async
            WaitOnScheduler();
        }

        [Given(@"we await synchronously")]
        public void GivenWeAwaitSynchronously() => WaitOnScheduler();

        [When(@"we set asynchronously")]
        public async Task WhenWeSetAsynchronously()
        {
            await Task.CompletedTask; // Just to make the step async
            Set();
        }

        [When(@"we set synchronously")]
        public void WhenWeSetSynchronously() => Set();

        [When(@"we wait a bit")]
        public async Task WhenWeWaitABit() => await Task.Delay(100);

        [Then(@"await should have completed")]
        public void ThenAwaitShouldHaveCompleted() => Assert.AreEqual(false, _waiting);
    }
}

My assumption is that when the await is called from an asynchronous test step then SynchronizationContext.Current is captured and when the task completes it tries to return on that SynchronizationContext

可以使用 ConfigureAwait(false) 进行更改。我一个人做了那个改变:

private async Task Wait()
{
    _tcs = new TaskCompletionSource();
    _waiting = true;
    await _tcs.Task.ConfigureAwait(false);
    _waiting = false;
}

并得到了这些结果,使用你的代码逐字减去那个变化。

关于必须一直调用 ConfigureAwait(false) 的问题,有几种解决方法。 useful documentation 所有这些,包括详细信息和避免捕获的方法,例如简单的 Task.Run 调用。

在这种情况下,最简单的方法是在设置场景时执行此操作:

SynchronizationContext.SetSynchronizationContext(null);

这完全消除了 ConfigureAwait 的需要,因为没有什么可捕获的。

还有其他选择。如果这不能回答您的问题,请参阅链接的文章或评论。

据我所知,没有公开的 属性 任何地方可以更改默认行为,因此无论如何都需要一些代码。

就是说 ConfigureAwait(false) 当其他编码人员阅读它时,您可以非常明确地说明您在做什么。所以如果你想要一个不同的解决方案,我会记住这一点。