我对用 MessageLoopWorker 包装的 WebBrowser 控件进行了一些测试,如下所述:WebBrowser Control in a new thread
但是当另一个测试创建用户控件或表单时,测试会卡住并且永远不会完成:
[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 wait 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 之后会阻塞。通过归零,它可以很好地通过。
一直这样下去可以吗?
最佳答案
我在 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.Wait
位于其返回的 Task
上。由于原始单元测试线程没有消息循环并且不泵送消息(与 WindowsFormsSynchronizationContext
所期望的不同),因此 NeverCompletes< 内的
永远没有机会执行,而 await
延续Task.Wait
只是挂着等待。
也就是说,MessageLoopWorker
仅设计用于在 async
方法范围内创建和运行 WinForms
对象> 您传递给 MessageLoopWorker.Run
,然后完成。例如,以下内容不会被阻止:
[TestMethod]
public async Task NeverCompletes()
{
await MessageLoopWorker.Run(async (args) =>
{
using (new Form()) ;
return Type.Missing;
});
}
它不设计用于跨多个 MessageLoopWorker.Run
调用使用 WinForms
对象。如果这正是您所需要的,您可能需要查看我的 MessageLoopApartment
(来自 here) ,例如:
[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());
}
}
}
或者,如果您不担心测试的潜在耦合,您甚至可以在多个单元测试方法中使用它,例如(MS测试):
[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
对象一起使用的(这无论如何,这几乎从来都不是一个好主意)。您可以拥有任意数量的 MessageLoopWorker
/MessageLoopApartment
实例,但是一旦在特定 的线程上创建了
/WinForm
对象, >MessageLoopWorkerMessageLoopApartment
实例,它应该仅在同一线程上进一步访问和正确销毁。
关于c# - 当在其之前创建 Form 时,带有应用程序循环的 NUnit 测试会挂起,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41533997/