验证是否正在等待任务

8
我有以下代码需要测试:

我有以下代码需要测试:

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() 的调用,它就会很简单:我只需删除async并将_keepAliveTask返回而不是等待它:

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

但这是不可行的(同步覆盖异步)。而且它仍然存在先前方法的问题。

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

虽然这是(有效的)性能提升,但会引入更多的复杂性: - 没有隐藏第二个异常(当两者都失败时)很难正确处理 - 允许并行执行

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

观察效果而不是验证调用

当然,我可以始终观察效果而不是"单元"测试方法调用等。也就是说: 只要_keepAliveTask没有结束,EndSession任务就不得结束。但是由于我不能无限期地等待,所以必须进行超时处理。测试应该很快,因此像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();
}

但是我真的非常不喜欢这种方法:

  • 难以理解
  • 不可靠的
  • 或者,当它相当可靠时,需要很长时间(这不应该是单元测试的特点)。

2
如果 _keepAliveTask 被取消并等待,它应该抛出 OperationCancelledException,不是吗?我不太确定你实际上想做什么。你想确保 LogOutIfPossible 被等待吗? - Yuval Itzchakov
你实际上想要测试什么?是 EndSession 是否完成? - i3arnon
@l3arnon EndSession 只有在 _keepAliveTask 完成后才会完成。 - BatteryBackupUnit
1
@YuvalItzchakov 是的,我认为这是一个可能的解决方案。让任务返回一个结果(或在没有结果的情况下抛出异常),并验证“包装”任务是否返回相同的结果(抛出该异常)。谢谢! - BatteryBackupUnit
@Krumelur 这不是关于测试框架是否支持异步测试方法的问题。我正在使用支持[Test]public async Task MyTestMethdo()的 NUnit,并且我经常使用它。 - BatteryBackupUnit
显示剩余4条评论
2个回答

4
如果您只想验证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;
    }
}

只要有一个带有 GetAwaiter 方法并返回具有 IsCompletedOnCompletedGetResult 属性的对象,你就可以使用虚假可等待项来确保 _keepAliveTask 被等待:

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

如果您使用某些模拟框架,您可以使 TaskGetAwaiter 方法返回我们的 MyAwaiter

我要试一下这个。不过还需要确保自定义的 MyAwaitable 实现正确... - BatteryBackupUnit

0
使用TaskCompletionSource并在已知时间设置其结果。 确认在设置结果之前,EndSession上的await尚未完成。 确认在设置结果后,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);

这基本上就是我所描述的观察效果而不是验证调用。这并不可靠。为了达到足够的可靠性,测试需要花费很长时间。在单元测试中等待一秒钟(或更长时间)实际上是不可接受的。除非能够并行运行它们,否则这不会成为一个大问题。 - BatteryBackupUnit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接