验证正在等待任务
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() 的调用,那将非常简单:我只需删除 async
和 return _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);
}
然而,有两个反对意见:
- 我有多项任务需要等待(我正在考虑
Task.WhenAll
更进一步)
- 这样做只会将等待任务的责任转移给
EndSession
的调用者。仍然必须在那里进行测试。
非异步方法,同步优于异步
当然,我可以做类似的事情:
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
, OnCompleted
和 GetResult
你可以使用虚拟等待来确保正在等待 _keepAliveTask
:
_keepAliveTask = new MyAwaitable();
EndSession();
_keepAliveTask.IsAwaited.Should().BeTrue();
如果您使用一些模拟框架,您可以将 Task
的 GetAwaiter
return 设为我们的 MyAwaiter
.
- 使用
TaskCompletionSource
并在已知时间设置其结果。
- 确认在设置结果之前,EndSession 上的等待尚未完成。
- 验证设置结果后,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);
我想测试以下代码:
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
和 return _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);
}
然而,有两个反对意见:
- 我有多项任务需要等待(我正在考虑
Task.WhenAll
更进一步) - 这样做只会将等待任务的责任转移给
EndSession
的调用者。仍然必须在那里进行测试。
非异步方法,同步优于异步
当然,我可以做类似的事情:
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
, OnCompleted
和 GetResult
你可以使用虚拟等待来确保正在等待 _keepAliveTask
:
_keepAliveTask = new MyAwaitable();
EndSession();
_keepAliveTask.IsAwaited.Should().BeTrue();
如果您使用一些模拟框架,您可以将 Task
的 GetAwaiter
return 设为我们的 MyAwaiter
.
- 使用
TaskCompletionSource
并在已知时间设置其结果。 - 确认在设置结果之前,EndSession 上的等待尚未完成。
- 验证设置结果后,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);