验证正在等待任务

Verify that task is being awaited

我想测试以下代码:

private Task _keepAliveTask; // get's assigned by object initializer

public async Task EndSession()
{
    _cancellationTokenSource.Cancel(); // cancels the _keepAliveTask
    await _logOutCommand.LogOutIfPossible();
    await _keepAliveTask;
}

重要的是 EndSession 任务仅在 `_keepAliveTask' 结束后结束。但是,我正在努力寻找一种可靠的测试方法。

问题: 如何对 EndSession 方法进行单元测试并验证 EndSession 返回的 Task 等待 _keepAliveTask.

出于演示目的,单元测试可能如下所示:

public async Task EndSession_MustWaitForKeepAliveTaskToEnd()
{
    var keepAliveTask = new Mock<Task>();
    // for simplicity sake i slightly differ from the other examples
    // by passing the task as method parameter

    await EndSession(keepAliveTask);

    keepAliveTask.VerifyAwaited(); // this is what i want to achieve
}

更多标准: - 可靠的测试(当实现正确时总是通过,当实现错误时总是失败) - 不能超过几毫秒(毕竟这是一个单元测试)。


我已经考虑了几个备选方案,我在下面记录了这些备选方案:

async方法

如果没有对 _logOutCommand.LogOutIfPossible() 的调用,那将非常简单:我只需删除 asyncreturn _keepAliveTask 而不是 await它:

public Task EndSession()
{
    _cancellationTokenSource.Cancel();
    return _keepAliveTask;
}

单元测试看起来(简化):

public void EndSession_MustWaitForKeepAliveTaskToEnd()
{
    var keepAliveTask = new Mock<Task>();
    // for simplicity sake i slightly differ from the other examples
    // by passing the task as method parameter

    Task returnedTask = EndSession(keepAliveTask);

    returnedTask.Should().be(keepAliveTask);
}

然而,有两个反对意见:

非异步方法,同步优于异步

当然,我可以做类似的事情:

public Task EndSession()
{
    _cancellationTokenSource.Cancel(); // cancels the _keepAliveTask
    _logOutCommand.LogOutIfPossible().Wait();
    return _keepAliveTask;
}

但那是不行的(同步优于异步)。加上它仍然存在以前方法的问题。

async方法使用Task.WhenAll(...)

是(有效的)性能改进,但引入了更多的复杂性: - 很难在不隐藏第二个异常的情况下做对(当两者都失败时) - 允许并行执行

由于性能在这里不是关键,因此我想避免额外的复杂性。此外,之前提到的问题,它只是将(验证)问题移至 EndSession 方法的调用者,这里也适用。

观察效果而不是验证调用

现在当然不是 "unit" 测试方法调用等。我总能观察到效果。即:只要 _keepAliveTask 还没有结束,EndSession Task 也不能结束。但是因为我不能无限期地等待,所以必须接受超时。测试应该很快,所以 5 秒这样的超时是不行的。所以我所做的是:

[Test]
public void EndSession_MustWaitForKeepAliveTaskToEnd()
{
    var keepAlive = new TaskCompletionSource<bool>();
    _cancelableLoopingTaskFactory
        .Setup(x => x.Start(It.IsAny<ICancelableLoopStep>(), It.IsAny<CancellationToken>()))
        .Returns(keepAlive.Task);

    _testee.StartSendingKeepAlive();

    _testee.EndSession()
            .Wait(TimeSpan.FromMilliseconds(20))
            .Should().BeFalse();
}

但我真的很不喜欢这种做法:

如果您只想验证 EndSession 正在等待 _keepAliveTask(并且您确实可以完全控制 _keepAliveTask),那么您可以创建自己的可等待类型而不是 Task 等待信号并检查:

public class MyAwaitable
{
    public bool IsAwaited;
    public MyAwaiter GetAwaiter()
    {
        return new MyAwaiter(this);
    }
}

public class MyAwaiter
{
    private readonly MyAwaitable _awaitable;

    public MyAwaiter(MyAwaitable awaitable)
    {
        _awaitable = awaitable;
    }

    public bool IsCompleted
    {
        get { return false; }
    }

    public void GetResult() {}

    public void OnCompleted(Action continuation)
    {
        _awaitable.IsAwaited = true;
    }
}

因为你只需要 await 有一个 GetAwaiter 方法 returns something with IsCompleted , OnCompletedGetResult 你可以使用虚拟等待来确保正在等待 _keepAliveTask:

_keepAliveTask = new MyAwaitable();
EndSession();
_keepAliveTask.IsAwaited.Should().BeTrue();

如果您使用一些模拟框架,您可以将 TaskGetAwaiter return 设为我们的 MyAwaiter.

  1. 使用 TaskCompletionSource 并在已知时间设置其结果。
  2. 确认在设置结果之前,EndSession 上的等待尚未完成。
  3. 验证设置结果后,EndSession 上的等待已完成。

简化版本可能如下所示(使用 nunit):

[Test]
public async Task VerifyTask()
{
    var tcs = new TaskCompletionSource<bool>();
    var keepAliveTask = tcs.Task;

    // verify pre-condition
    Assert.IsFalse(keepAliveTask.IsCompleted);

    var waitTask = Task.Run(async () => await keepAliveTask);

    tcs.SetResult(true);

    await waitTask;

    // verify keepAliveTask has finished, and as such has been awaited
    Assert.IsTrue(keepAliveTask.IsCompleted);
    Assert.IsTrue(waitTask.IsCompleted); // not needed, but to make a point
}

您还可以在 waitTask 中添加一个短暂的延迟,以确保任何同步执行都会更快,例如:

var waitTask = Task.Run(async () =>
{
    await Task.Delay(1);
    await keepAliveTask;
 });

如果您不相信您的单元测试框架能够正确处理异步,您可以将完成标志设置为 waitTask 的一部分,并在最后检查它。类似于:

bool completed = false;
var waitTask = Task.Run(async () =>
{
    await Task.Delay(1);
    await keepAliveTask;
    completed = true;
 });

 // { .... }

 // at the end of the method
 Assert.IsTrue(completed);