vyr*_*yrp 7 c# unit-testing async-await
我刚看过Jon Skeet的视频课程,他在那里讨论了异步方法的单元测试.它是在付费网站上,但我在他的书中找到了类似于他所说的内容(只有Ctrl + F"15.6.3.单元测试异步代码").
完整的代码可以在他的github上找到,但为了我的问题,我已经简化了它(我的代码基本上是StockBrokerTest.CalculateNetWorthAsync_AuthenticationFailure_ThrowsDelayed()在内联TimeMachine和Advancer操作).
假设我们有一个类来测试失败的登录(没有单元测试框架来简化问题):
public static class LoginTest
{
private static TaskCompletionSource<Guid?> loginPromise = new TaskCompletionSource<Guid?>();
public static void Main()
{
Console.WriteLine("== START ==");
// Set up
var context = new ManuallyPumpedSynchronizationContext(); // Comment this
SynchronizationContext.SetSynchronizationContext(context); // Comment this
// Run method under test
var result = MethodToBeTested();
Debug.Assert(!result.IsCompleted, "Result should not have been completed yet.");
// Advancing time
Console.WriteLine("Before advance");
loginPromise.SetResult(null);
context.PumpAll(); // Comment this
Console.WriteLine("After advance");
// Check result
Debug.Assert(result.IsFaulted, "Result should have been faulted.");
Debug.Assert(result.Exception.InnerException.GetType() == typeof(ArgumentException), $"The exception should have been of type {nameof(ArgumentException)}.");
Console.WriteLine("== END ==");
Console.ReadLine();
}
private static async Task<int> MethodToBeTested()
{
Console.WriteLine("Before login");
var userId = await Login();
Console.WriteLine("After login");
if (userId == null)
{
throw new ArgumentException("Bad username or password");
}
return userId.GetHashCode();
}
private static Task<Guid?> Login()
{
return loginPromise.Task;
}
}
Run Code Online (Sandbox Code Playgroud)
执行的地方ManuallyPumpedSynchronizationContext是:
public sealed class ManuallyPumpedSynchronizationContext : SynchronizationContext
{
private readonly BlockingCollection<Tuple<SendOrPostCallback, object>> callbacks;
public ManuallyPumpedSynchronizationContext()
{
callbacks = new BlockingCollection<Tuple<SendOrPostCallback, object>>();
}
public override void Post(SendOrPostCallback callback, object state)
{
Console.WriteLine("Post()");
callbacks.Add(Tuple.Create(callback, state));
}
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("Synchronous operations not supported on ManuallyPumpedSynchronizationContext");
}
public void PumpAll()
{
Tuple<SendOrPostCallback, object> callback;
while(callbacks.TryTake(out callback))
{
Console.WriteLine("PumpAll()");
callback.Item1(callback.Item2);
}
}
}
Run Code Online (Sandbox Code Playgroud)
输出是:
== START ==
Before login
Before advance
After login
After advance
== END ==
Run Code Online (Sandbox Code Playgroud)
我的问题是:为什么我们需要ManuallyPumpedSynchronizationContext?
为什么默认的SynchronizationContext不够用?Post()甚至不调用该方法(基于输出).我已经尝试评论标记为的行// Comment this,输出是相同的,并且断言通过.
如果我正确理解Jon Skeet在视频中所说的内容,那么SynchronizationContext.Post()当我们遇到一个await尚未完成的任务时,应该调用该方法.但这种情况并非如此.我错过了什么?
有条件的信息
通过我的研究,我偶然发现了这个答案.为了尝试它,我将Login()方法的实现更改为:
private static Task<Guid?> Login()
{
// return loginPromise.Task;
return Task<Guid?>.Factory.StartNew(
() =>
{
Console.WriteLine("Login()");
return null;
},
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
Run Code Online (Sandbox Code Playgroud)
通过这种修改,Post()确实调用了该方法.输出:
== START ==
Before login
Post()
Before advance
PumpAll()
Login()
After login
After advance
== END ==
Run Code Online (Sandbox Code Playgroud)
所以用Jon Skeet的使用TaskCompletionSource,他的创作是ManuallyPumpedSynchronizationContext不是必须的?
注意:我认为我看到的视频是在C#5发布日期前完成的.
在这种情况下,SetResult正在同步(直接)执行其延续。这是由于一些未记录的细节造成的:
await将用该标志安排其延续TaskContinuationOption.ExecuteSynchronously。当我第一次发现这种行为时,我将其报告为错误。虽然我仍然认为异步延续并不令人惊讶,但有一个有效的效率论点支持同步执行。await捕获 a时,如果当前实例与捕获的实例相同(引用相等),SynchronizationContext则它将允许同步延续。同样,这是出于性能原因;SyncCtx 实例上的相等性没有明确定义,但这在现实世界中效果很好。SynchronizationContextSynchronizationContext因此,您看到此行为是因为在该SetResult行,被设置为由inSynchronizationContext.Current捕获的相同 SyncCtx 。awaitMethodToBeTested
更实际的示例是在调用被测系统后清除当前的 SyncCtx。因此,单元测试代码并不存在于 SyncCtx“内部”;它只为被测系统提供SyncCtx:
...
// Set up
var context = new ManuallyPumpedSynchronizationContext(); // Comment this
SynchronizationContext.SetSynchronizationContext(context); // Comment this
// Run method under test
var result = MethodToBeTested();
Debug.Assert(!result.IsCompleted, "Result should not have been completed yet.");
// Tear down SyncCtx.
SynchronizationContext.SetSynchronizationContext(null);
// Advancing time
...
Run Code Online (Sandbox Code Playgroud)
或者,您可以传递TaskCreationOptions.RunContinuationsAsynchronously给TaskCompletionSource<T>构造函数。但是,请注意.NET Framework 中当前存在的此错误将阻止其在全桌面控制台应用程序上运行;它仅适用于 .NET Core 控制台应用程序。
或者,当然,您可以将其包装SetResult在Task.Run:
Task.Run(() => loginPromise.SetResult(null)).Wait();
Run Code Online (Sandbox Code Playgroud)
这会强制在线程池线程上继续(没有 SyncCtx),因此继续必须调用Post.
最后一点,您可能想使用AsyncContextAsyncEx 库中的我的类型;这是一种更加充实的习俗SynchronizationContext,将自己与特定的线索联系在一起。我最初编写AsyncContext是为了与单元测试一起使用。当 SUT 具有异步代码时,它通常需要 SyncCtx。事实上,xUnit在测试框架中提供了自己的内置权利。