【问题标题】: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);
        }
      }
      

      【讨论】:

        【解决方案3】:

        您可以创建自定义 SynchronizationContext 派生类并将其注册为 SynchronizationContext.SetSynchronizationContext 的当前上下文。阅读 Stephen Toub 在“Await, SynchronizationContext, and Console Apps”和“Await, SynchronizationContext, and Console Apps: Part 2”上的帖子。

        您的自定义 SynchronizationContext 只需覆盖接收异步执行回调的 Post 方法。如何执行它们取决于您。

        第一个帖子提供了一个同步上下文,它将所有已发布的操作存储在一个队列中,以及一个阻塞循环,该循环从队列中获取操作并在单个线程中执行它们。

        【讨论】:

          【解决方案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);
                  }
              }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-02-13
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-04-12
            • 1970-01-01
            相关资源
            最近更新 更多