2017-01-08 57 views
2

我有包裹着MessageLoopWorker WebBrowser控件的几个测试,这里描述的创建形式,它挂起:WebBrowser Control in a new threadNUnit测试与应用回路时前

但是,当另一个测试创建用户控制或形式,测试冻结并无法完成:

[Test] 
    public async Task WorksFine() 
    { 
     await MessageLoopWorker.Run(async() => new {}); 
    } 

    [Test] 
    public async Task NeverCompletes() 
    { 
     using (new Form()) ; 
     await MessageLoopWorker.Run(async() => new {}); 
    } 

    // a helper class to start the message loop and execute an asynchronous task 
    public static class MessageLoopWorker 
    { 
     public static async Task<object> Run(Func<object[], Task<object>> worker, params object[] args) 
     { 
      var tcs = new TaskCompletionSource<object>(); 

      var thread = new Thread(() => 
      { 
       EventHandler idleHandler = null; 

       idleHandler = async (s, e) => 
       { 
        // handle Application.Idle just once 
        Application.Idle -= idleHandler; 

        // return to the message loop 
        await Task.Yield(); 

        // and continue asynchronously 
        // propogate the result or exception 
        try 
        { 
         var result = await worker(args); 
         tcs.SetResult(result); 
        } 
        catch (Exception ex) 
        { 
         tcs.SetException(ex); 
        } 

        // signal to exit the message loop 
        // Application.Run will exit at this point 
        Application.ExitThread(); 
       }; 

       // handle Application.Idle just once 
       // to make sure we're inside the message loop 
       // and SynchronizationContext has been correctly installed 
       Application.Idle += idleHandler; 
       Application.Run(); 
      }); 

      // set STA model for the new thread 
      thread.SetApartmentState(ApartmentState.STA); 

      // start the thread and await for the task 
      thread.Start(); 
      try 
      { 
       return await tcs.Task; 
      } 
      finally 
      { 
       thread.Join(); 
      } 
     } 
    } 

代码的步骤,以及在除return await tcs.Task;永远不会返回。

包装new Form到MessageLoopWorker.Run(...)似乎使它更好,但它不适用于更复杂的代码,不幸的是。我还有很多其他的测试,包括表单和用户控件,我希望避免包装到messageloopworker中。

也许MessageLoopWorker可以修复以避免干扰其他测试?

更新:继@Noseratio的惊人答案我在MessageLoopWorker.Run调用之前重置了同步上下文,现在它运行良好。

更多有意义的代码:

[Test] 
public async Task BasicControlTests() 
{ 
    var form = new CustomForm(); 
    form.Method1(); 
    Assert.... 
} 

[Test] 
public async Task BasicControlTests() 
{ 
    var form = new CustomForm(); 
    form.Method1(); 
    Assert.... 
} 

[Test] 
public async Task WebBrowserExtensionTest() 
{ 
    SynchronizationContext.SetSynchronizationContext(null); 

    await MessageLoopWorker.Run(async() => { 
     var browser = new WebBrowser(); 
     // subscribe on browser's events 
     // do something with browser 
     // assert the event order 
    }); 
} 

当运行测试,而归零同步上下文WebBrowserExtensionTest块时它遵循BasicControlTests。通过零位它传递良好。

可以保持这样吗?

回答

3

我在MSTest下重新评估了这个,但我相信下面的所有内容同样适用于NUnit。

首先,我明白这段代码可能已经脱离了上下文,但是现在看起来并不是很有用。为什么要在NeverCompletes内创建一个表单,该表单在随机的MSTest/NUnit线程上运行,与MessageLoopWorker产生的线程不同?

无论如何,你遇到了死锁,因为using (new Form())在该原始单元测试线程上安装了一个WindowsFormsSynchronizationContext的实例。在using声明后检查SynchronizationContext.Current。然后,你面对一个典型的僵局,并由Stephen Cleary在他的"Don't Block on Async Code"中做了很好的解释。

对,您没有阻止自己,但MSTest的/ NUnit的呢,因为它是足够聪明,认识到NeverCompletes方法async Task签名,然后由它返回的Task执行类似Task.Wait。由于原始单元测试线程没有消息循环并且不会收集消息(与WindowsFormsSynchronizationContext不同),NeverCompletes内的await延续永远不会有机会执行,并且Task.Wait正在等待。

这就是说,MessageLoopWorker仅为设计创建和运行async方法传递给MessageLoopWorker.Run的范围内WinForms对象,然后来完成。例如。,下面不会阻碍:

[TestMethod] 
public async Task NeverCompletes() 
{ 
    await MessageLoopWorker.Run(async (args) => 
    { 
     using (new Form()) ; 
     return Type.Missing; 
    }); 
} 

设计有多个WinForms对象MessageLoopWorker.Run通话。如果这是你需要什么,你可能想看看我的MessageLoopApartmenthere,如:

[TestMethod] 
public async Task NeverCompletes() 
{ 
    using (var apartment = new MessageLoopApartment()) 
    { 
     // create a form inside MessageLoopApartment 
     var form = apartment.Invoke(() => new Form { 
      Width = 400, Height = 300, Left = 10, Top = 10, Visible = true }); 

     try 
     { 
      // await outside MessageLoopApartment's thread 
      await Task.Delay(2000); 

      await apartment.Run(async() => 
      { 
       // this runs on MessageLoopApartment's STA thread 
       // which stays the same for the life time of 
       // this MessageLoopApartment instance 

       form.Show(); 
       await Task.Delay(1000); 
       form.BackColor = System.Drawing.Color.Green; 
       await Task.Delay(2000); 
       form.BackColor = System.Drawing.Color.Red; 
       await Task.Delay(3000); 

      }, CancellationToken.None); 
     } 
     finally 
     { 
      // dispose of WebBrowser inside MessageLoopApartment 
      apartment.Invoke(() => form.Dispose()); 
     } 
    } 
} 

或者,你甚至可以用它在多个单元测试方法,如果你不关心的测试潜在的耦合,例如(MSTest的):

[TestClass] 
public class MyTestClass 
{ 
    static MessageLoopApartment s_apartment; 

    [ClassInitialize] 
    public static void TestClassSetup() 
    { 
     s_apartment = new MessageLoopApartment(); 
    } 

    [ClassCleanup] 
    public void TestClassCleanup() 
    { 
     s_apartment.Dispose(); 
    } 

    // ... 
} 

最后,既不MessageLoopWorker也不MessageLoopApartment设计与不同的线程(这是几乎从来没有一个好主意,反正)创建WinForms对象的工作。只要您喜欢,您可以拥有尽可能多的实例,但是一旦在特定实例的线程上创建了一个对象,它应该只在同一个线程上被进一步访问和正确销毁。

+1

令人惊叹的是,它是超级有用的。谢谢 - 我现在更了解它。我已经添加了上下文,希望它会没事的。 –

+0

@AlexAtNet,如果有帮助,很高兴。我仍然不会在MessageLoopWorker的线程之外创建任何WinForms对象。在大多数情况下,它们需要消息循环。至少,如果你这样做,一定要配置NUnit给你一个STA线程。 – Noseratio