[英]NUnit test with Application loop in it hangs when Form is created before it
I have a few tests with WebBrowser control wrapped with MessageLoopWorker as described here: WebBrowser Control in a new thread 我对WebBrowser控件进行了一些测试,并用MessageLoopWorker包装,如下所述: 新线程中的WebBrowser控件
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();
}
}
}
The code steps-in well except return await tcs.Task;
代码
return await tcs.Task;
很好,除了return await tcs.Task;
never returns. 永远不会回来。
Wrapping new Form
into the MessageLoopWorker.Run(...) seems to make it better, but it does not work with more complicated code, unfortunately. 将
new Form
包装到MessageLoopWorker.Run(...)中似乎使它更好,但是不幸的是,它不适用于更复杂的代码。 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? 也许可以固定MessageLoopWorker以避免与其他测试产生干扰?
Update: following the amazing answer by @Noseratio I've reset the synchronisation context before the MessageLoopWorker.Run call and it now works well. 更新:按照@Noseratio给出的令人惊奇的答案,我已经在MessageLoopWorker.Run调用之前重置了同步上下文,现在它运行良好。
More meaningful code: 更有意义的代码:
[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
});
}
When running the tests without nulling the sync context WebBrowserExtensionTest blocks when it follows BasicControlTests. 如果运行测试时没有使同步上下文无效,则WebBrowserExtensionTest在遵循BasicControlTests时会阻塞。 With nulling it pass well.
使用null可以顺利通过。
Is it ok to keep it like this? 这样保留它可以吗?
I repro'ed this under MSTest, but I believe all of the below applies to NUnit equally well. 我在MSTest下对此进行了重现,但我相信以下所有内容同样适用于NUnit。
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
? 为什么要在
NeverCompletes
内创建一个窗体,该窗体运行在与MessageLoopWorker
生成的线程不同的随机MSTest / NUnit线程上?
Anyhow, you're having a deadlock because using (new Form())
installs an instance of WindowsFormsSynchronizationContext
on that original unit test thread. 无论如何,您陷入了僵局,因为
using (new Form())
在该原始单元测试线程上安装WindowsFormsSynchronizationContext
的实例。 Check SynchronizationContext.Current
after the using
statement. 在
using
语句之后检查SynchronizationContext.Current
。 Then, you facing a classic deadlock well explained by Stephen Cleary in his "Don't Block on Async Code" . 然后,您将面临一个经典的僵局,该僵局由Stephen Cleary在他的“不要阻止异步代码”中很好地解释。
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. 是的,您不会阻塞自己,但是MSTest / NUnit会阻塞,因为它足够聪明,可以识别
NeverCompletes
方法的async Task
签名,然后Task.Wait
返回的Task
执行类似Task.Wait
的操作。 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. 因为原始的单元测试线程没有消息循环,也没有泵送消息(这与
WindowsFormsSynchronizationContext
所期望的不同),所以NeverCompletes
内部的await
延续永远不会执行, Task.Wait
只是挂起了等待。
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. 就是说,
MessageLoopWorker
仅设计为在传递给MessageLoopWorker.Run
的async
方法范围内创建并运行WinForms
对象,然后完成该操作。 Eg, the following wouldn't block: 例如,以下内容不会被阻止:
[TestMethod]
public async Task NeverCompletes()
{
await MessageLoopWorker.Run(async (args) =>
{
using (new Form()) ;
return Type.Missing;
});
}
It was not designed to work with WinForms
objects across multiple MessageLoopWorker.Run
calls. 它并非旨在与多个
MessageLoopWorker.Run
调用中的WinForms
对象一起使用。 If that's what you need, you may want to look at my MessageLoopApartment
from here , eg: 如果这是您需要的,则可能需要从这里查看我的
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());
}
}
}
Or, you can even use it across multiple unit test methods, if you're not concerned about potential coupling of tests, eg (MSTest): 或者,如果您不担心测试的潜在耦合,甚至可以跨多种单元测试方法使用它,例如(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();
}
// ...
}
Finally, neither MessageLoopWorker
nor MessageLoopApartment
were designed to work with WinForms
object created on different threads (which is almost never a good idea anyway). 最后,
MessageLoopWorker
和MessageLoopApartment
都没有设计为与在不同线程上创建的WinForms
对象一起使用(无论如何这几乎不是一个好主意)。 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. 您可以根据需要拥有任意多个
MessageLoopWorker
/ MessageLoopApartment
实例,但是一旦在特定MessageLoopWorker
/ MessageLoopApartment
实例的线程上创建了WinForm
对象,则应进一步对其进行访问并仅在同一线程上对其进行适当销毁。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.