【问题标题】:how to add synchronization context to async test method如何将同步上下文添加到异步测试方法
【发布时间】:2020-12-04 18:10:03
【问题描述】:
我有 Visual Studio 2012 和一个需要同步上下文的异步测试。
但是 MSTest 的默认同步上下文为 null。
我想测试在具有同步上下文的 WPF 或 WinForms-UI 线程上运行。
将 SynchronizationContext 添加到测试线程的最佳方法是什么?
[TestMethod]
public async Task MyTest()
{
Assert.IsNotNull( SynchronizationContext.Current );
await MyTestAsync();
DoSomethingOnTheSameThread();
}
【问题讨论】:
标签:
c#
mstest
async-await
【解决方案1】:
您可以在我的AsyncEx library 中使用名为AsyncContext 的单线程SynchronizationContext:
[TestMethod]
public void MyTest()
{
AsyncContext.Run(async () =>
{
Assert.IsNotNull( SynchronizationContext.Current );
await MyTestAsync();
DoSomethingOnTheSameThread();
});
}
但是,这并不能完全伪造特定的 UI 环境,例如,Dispatcher.CurrentDispatcher 仍将是 null。如果您需要这种级别的伪造,您应该使用原始 Async CTP 中的 SynchronizationContext 实现。它附带了三个可用于测试的 SynchronizationContext 实现:一个通用的(类似于我的 AsyncContext),一个用于 WinForms,一个用于 WPF。
【解决方案2】:
使用 Panagiotis Kanavos 和 Stephen Cleary 提供的信息,我可以像这样编写我的测试方法:
[TestMethod]
public void MyTest()
{
Helper.RunInWpfSyncContext( async () =>
{
Assert.IsNotNull( SynchronizationContext.Current );
await MyTestAsync();
DoSomethingOnTheSameThread();
});
}
内部代码现在在 WPF 同步上下文中运行,并处理用于 MSTest 的所有异常。
Helper 方法来自 Stephen Toub:
using System.Windows.Threading; // WPF Dispatcher from assembly 'WindowsBase'
public static void RunInWpfSyncContext( Func<Task> function )
{
if (function == null) throw new ArgumentNullException("function");
var prevCtx = SynchronizationContext.Current;
try
{
var syncCtx = new DispatcherSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
var task = function();
if (task == null) throw new InvalidOperationException();
var frame = new DispatcherFrame();
var t2 = task.ContinueWith(x=>{frame.Continue = false;}, TaskScheduler.Default);
Dispatcher.PushFrame(frame); // execute all tasks until frame.Continue == false
task.GetAwaiter().GetResult(); // rethrow exception when task has failed
}
finally
{
SynchronizationContext.SetSynchronizationContext(prevCtx);
}
}
【解决方案4】:
可以声明您自己的测试方法属性,您可以在其中在测试运行中注入自定义代码。使用它,您可以用您自己的 [SynchronizationContextTestMethod] 替换 [TestMethod] 属性,该属性会自动使用上下文集运行测试(仅在 VS2019 中测试):
public class SynchronizationContextTestMethodAttribute : TestMethodAttribute
{
public override TestResult[] Execute(ITestMethod testMethod)
{
Func<Task> function = async () =>
{
var declaringType = testMethod.MethodInfo.DeclaringType;
var instance = Activator.CreateInstance(declaringType);
await InvokeMethodsWithAttribute<TestInitializeAttribute>(instance, declaringType);
await (Task)testMethod.MethodInfo.Invoke(instance, null);
await InvokeMethodsWithAttribute<TestCleanupAttribute>(instance, declaringType);
};
var result = new TestResult();
result.Outcome = UnitTestOutcome.Passed;
var stopwatch = Stopwatch.StartNew();
try
{
RunInSyncContext(function);
}
catch (Exception ex)
{
result.Outcome = UnitTestOutcome.Failed;
result.TestFailureException = ex;
}
result.Duration = stopwatch.Elapsed;
return new[] { result };
}
private static async Task InvokeMethodsWithAttribute<A>(object instance, Type declaringType) where A : Attribute
{
if (declaringType.BaseType != typeof(object))
await InvokeMethodsWithAttribute<A>(instance, declaringType.BaseType);
var methods = declaringType.GetMethods(BindingFlags.Instance | BindingFlags.Public);
foreach (var methodInfo in methods)
if (methodInfo.DeclaringType == declaringType && methodInfo.GetCustomAttribute<A>() != null)
{
if (methodInfo.ReturnType == typeof(Task))
{
var task = (Task)methodInfo.Invoke(instance, null);
if (task != null)
await task;
}
else
methodInfo.Invoke(instance, null);
}
}
public static void RunInSyncContext(Func<Task> function)
{
if (function == null)
throw new ArgumentNullException(nameof(function));
var prevContext = SynchronizationContext.Current;
try
{
var syncContext = new DispatcherSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncContext);
var task = function();
if (task == null)
throw new InvalidOperationException();
var frame = new DispatcherFrame();
var t2 = task.ContinueWith(x => { frame.Continue = false; }, TaskScheduler.Default);
Dispatcher.PushFrame(frame); // execute all tasks until frame.Continue == false
task.GetAwaiter().GetResult(); // rethrow exception when task has failed
}
finally
{
SynchronizationContext.SetSynchronizationContext(prevContext);
}
}
}