在创建Form之前,带有Application循环的NUnit测试会挂起 [英] NUnit test with Application loop in it hangs when Form is created before it
问题描述
我对使用MessageLoopWorker包装的WebBrowser控件进行了一些测试,如下所述:
I have a few tests with WebBrowser control wrapped with MessageLoopWorker as described here: WebBrowser Control in a new thread
但是,当另一个测试创建用户控件或表单时,该测试将冻结并且永远不会完成:
But when another test creates user control or form, the test freezes and never completes:
[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;
之外的所有代码都无法返回.
The code steps-in well except return await tcs.Task;
never returns.
将new Form
包装到MessageLoopWorker.Run(...)中似乎使它更好,但是不幸的是,它不适用于更复杂的代码.而且,我还有很多其他关于表单和用户控件的测试,希望避免将它们包装到messageloopworker中.
Wrapping new Form
into the MessageLoopWorker.Run(...) seems to make it better, but it does not work with more complicated code, unfortunately. And I have quite a lot of other tests with forms and user controls that I would like to avoid wrapping into messageloopworker.
也许可以固定MessageLoopWorker以避免与其他测试产生干扰?
Maybe MessageLoopWorker can be fixed to avoid the interference with other tests?
更新:在@Noseratio给出了一个令人惊奇的答案之后,我已经在MessageLoopWorker.Run调用之前重置了同步上下文,并且现在运行良好.
Update: following the amazing answer by @Noseratio I've reset the synchronisation context before the MessageLoopWorker.Run call and it now works well.
更有意义的代码:
[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时会阻塞.使用null可以顺利通过.
When running the tests without nulling the sync context WebBrowserExtensionTest blocks when it follows BasicControlTests. With nulling it pass well.
可以这样保存吗?
推荐答案
我在MSTest下对此进行了复制,但是我相信以下所有内容同样适用于NUnit.
I repro'ed this under MSTest, but I believe all of the below applies to NUnit equally well.
首先,我了解这段代码可能已经脱离上下文了,但是就目前而言,它似乎不是很有用.为什么要在NeverCompletes
中创建一个在随机MSTest/NUnit线程上运行的窗体,该窗体与MessageLoopWorker
生成的线程不同?
First off all, I understand this code might have been taken out of context, but as is, it doesn't seem to be very useful. Why would you want to create a form inside NeverCompletes
, which runs on an random MSTest/NUnit thread, different from the thread spawned by MessageLoopWorker
?
无论如何,您遇到了死锁,因为using (new Form())
在该原始单元测试线程上安装了WindowsFormsSynchronizationContext
的实例.在using
语句之后检查SynchronizationContext.Current
.然后,您面对的经典死锁由Stephen Cleary在他的.
Anyhow, you're having a deadlock because using (new Form())
installs an instance of WindowsFormsSynchronizationContext
on that original unit test thread. Check SynchronizationContext.Current
after the using
statement. Then, you facing a classic deadlock well explained by Stephen Cleary in his "Don't Block on Async Code".
对,您不会阻塞自己,但是MSTest/NUnit会阻塞,因为它足够聪明,可以识别NeverCompletes
方法的async Task
签名,然后对它返回的Task
执行类似Task.Wait
的操作.因为原始的单元测试线程没有消息循环,也没有泵送消息(与WindowsFormsSynchronizationContext
期望的不同),所以NeverCompletes
内部的await
延续永远不会有执行的机会,而Task.Wait
只是挂等待.
Right, you don't block yourself but MSTest/NUnit does, because it is smart enough to recognize async Task
signature of NeverCompletes
method and then execute something like Task.Wait
on the Task
returned by it. Because the original unit test thread doesn't have a message loop and doesn't pump messages (unlike is expected by WindowsFormsSynchronizationContext
), the await
continuation inside NeverCompletes
never gets a chance to execute and Task.Wait
is just hanging waiting.
也就是说,MessageLoopWorker
仅设计用于在传递给MessageLoopWorker.Run
的async
方法范围内创建和运行WinForms
对象,然后完成.例如,以下内容不会被阻止:
That said, MessageLoopWorker
was only designed to create and run WinForms
object inside the scope of the async
method you pass to MessageLoopWorker.Run
, and then be done. E.g., the following wouldn't block:
[TestMethod]
public async Task NeverCompletes()
{
await MessageLoopWorker.Run(async (args) =>
{
using (new Form()) ;
return Type.Missing;
});
}
不旨在与多个MessageLoopWorker.Run
调用中的WinForms
对象一起使用.如果您需要的是,您可能想在此处查看我的MessageLoopApartment
,例如:
It was not designed to work with WinForms
objects across multiple MessageLoopWorker.Run
calls. If that's what you need, you may want to look at my MessageLoopApartment
from here, e.g.:
[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):
Or, you can even use it across multiple unit test methods, if you're not concerned about potential coupling of tests, e.g. (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
对象一起工作(无论如何这几乎不是一个好主意).您可以根据需要设置任意多个MessageLoopWorker
/MessageLoopApartment
实例,但是一旦在特定MessageLoopWorker
/MessageLoopApartment
实例的线程上创建了WinForm
对象,则应进一步对其进行访问并进行适当销毁只能在同一线程上.
Finally, neither MessageLoopWorker
nor MessageLoopApartment
were designed to work with WinForms
object created on different threads (which is almost never a good idea anyway). You can have as many MessageLoopWorker
/MessageLoopApartment
instances as you like, but once a WinForm
object has been created on the thread of a particular MessageLoopWorker
/MessageLoopApartment
instance, it should further be accessed and properly destroyed on the same thread only.
这篇关于在创建Form之前,带有Application循环的NUnit测试会挂起的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!