在其之前创建Form时,其中包含Application循环的NUnit测试会挂起

我有一些使用MessageLoopWorker包装的WebBrowser控件的测试,如下所述: WebBrowser Control在一个新线程中

但是当另一个测试创建用户控件或表单时,测试会冻结并永远不会完成:

[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 Run(Func<object[], Task> worker, params object[] args) { var tcs = new TaskCompletionSource(); 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; – 除了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 }); } 

在运行测试时,如果不遵循BasicControlTests,则不会使同步上下文WebBrowserExtensionTest阻塞。 通过归零它传递得很好。

可以保持这样吗?

我在MSTest下重申了这一点,但我相信以下所有内容同样适用于NUnit。

首先,我理解这段代码可能已经脱离了上下文,但是它看起来似乎并不是非常有用。 为什么要在NeverCompletes创建一个表单,它在随机MSTest / NUnit线程上运行,与MessageLoopWorker生成的线程不同?

无论如何,你有一个死锁因为using (new Form())在原始的unit testing线程上安装了一个WindowsFormsSynchronizationContext实例。 using语句后检查SynchronizationContext.Current 。 然后,你面对斯蒂芬克莱里在他的“不要阻止异步代码”中解释的经典死锁。

是的,你不会阻止自己,但MSTest / NUnit会这样做,因为它足够智能识别NeverCompletes方法的async Task签名,然后在它返回的Task上执行类似Task.Wait的操作。 因为原始的unit testing线程没有消息循环而且没有泵消息(与WindowsFormsSynchronizationContext ), NeverCompletesawait继续永远不会有机会执行,而Task.Wait只是挂起等待。

也就是说, MessageLoopWorker只是为了在传递给MessageLoopWorker.Run async方法的范围内创建和运行WinForms对象,然后完成。 例如,以下内容不会阻止:

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

不适用于跨多个MessageLoopWorker.Run调用的WinForms对象。 如果这就是您所需要的,您可以从这里查看我的MessageLoopApartment ,例如:

 [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()); } } } 

或者,如果您不关心测试的潜在耦合,您甚至可以在多个unit testing方法中使用它,例如(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(); } // ... } 

最后, MessageLoopWorkerMessageLoopApartment都不是设计用于在不同线程上创建的WinForms对象(无论如何这几乎都不是一个好主意)。 您可以拥有任意数量的MessageLoopWorker / MessageLoopApartment实例,但是一旦在特定的MessageLoopWorker / MessageLoopApartment实例的线程上创建了WinForm对象,就应该在同一个线程上进一步访问和正确销毁它。