【问题标题】:Verify that task is being awaited验证是否正在等待任务
【发布时间】:2015-03-17 11:25:15
【问题描述】:

我想测试以下代码:

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 而不是 awaiting 它:

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

但是,有两个反对意见:

  • 我有多个任务需要等待(我正在考虑Task.WhenAll 进一步向下)
  • 这样做只会将等待任务的责任转移给EndSession 的调用者。仍然需要在那里进行测试。

非异步方法,通过异步同步

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

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

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

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

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

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

观察效果而不是验证调用

现在当然不是“单元”测试方法调用等。我总是可以观察效果。也就是说:只要_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();
}

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

  • 很难理解
  • 不可靠
  • 或者 - 当它相当可靠时 - 它需要很长时间(单元测试不应该)。

【问题讨论】:

  • 如果_keepAliveTask 被取消并等待,它应该抛出一个OperationCancelledException,不是吗?我不太确定您实际上要做什么。您想确保等待LogOutIfPossible
  • 您实际上想测试什么? EndSession 完成了吗?
  • @l3arnon EndSession 只在 _keepAliveTask 完成后完成
  • @YuvalItzchakov 是的,我认为这是一个可能的解决方案。让任务返回结果(或在没有结果的情况下抛出异常)并验证“包装”任务是否返回相同的结果(抛出该异常)。谢谢!
  • 你使用什么测试框架? MS Test 支持异步测试方法。

标签: c# .net unit-testing task-parallel-library async-await


【解决方案1】:

如果您只想验证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 方法返回 somethingIsCompletedOnCompletedOnCompletedGetResult 你可以使用虚拟等待确定 _keepAliveTask 正在等待中:

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

如果您使用一些模拟框架,您可以改为让TaskGetAwaiter 返回我们的MyAwaiter

【讨论】:

  • 我要试试这个。仍然需要确保自定义的MyAwaitable 被正确实现,虽然...
【解决方案2】:
  1. 使用TaskCompletionSource 并将其结果设置在已知时间。
  2. 在设置结果之前,请确认 EndSession 上的等待尚未完成。
  3. 验证设置结果后,EndSession上的await已经完成。

简化版本可能如下所示(使用 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);

【讨论】:

  • 这基本上是我用观察效果而不是验证调用所描述的。这不可靠。为了获得足够的可靠性,测试必须花费很长时间。在单元测试中,等待一秒钟(或更多)是不可接受的。除非可以并行运行它们,否则这不是什么大问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-02-11
  • 2019-07-22
  • 2016-03-20
  • 2019-06-05
  • 1970-01-01
  • 1970-01-01
  • 2014-09-06
相关资源
最近更新 更多