C# 在窗体之前创建窗体时,应用程序循环在其中的NUnit测试将挂起
我对使用MessageLoopWorker包装的WebBrowser控件进行了一些测试,如下所述: 但当另一个测试创建用户控件或窗体时,该测试将冻结且永远不会完成:C# 在窗体之前创建窗体时,应用程序循环在其中的NUnit测试将挂起,c#,.net,winforms,async-await,nunit,C#,.net,Winforms,Async Await,Nunit,我对使用MessageLoopWorker包装的WebBrowser控件进行了一些测试,如下所述: 但当另一个测试创建用户控件或窗体时,该测试将冻结且永远不会完成: [Test] public async Task WorksFine() { await MessageLoopWorker.Run(async () => new {}); } [Test] public async Task NeverCompletes()
[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();
}
}
}
运行测试时,如果同步上下文WebBrowserExtensionTest块在BasicControlTests之后不为空,则执行WebBrowserExtensionTest块。如果将其置零,则可以顺利通过
这样保存可以吗 我在MSTest中重新解释了这一点,但我相信以下所有内容同样适用于NUnit 首先,我知道这段代码可能是断章取义的,但实际上,它似乎不是很有用。为什么要在
NeverCompletes
中创建一个表单,它运行在随机的MSTest/NUnit线程上,与MessageLoopWorker
生成的线程不同
无论如何,您正处于死锁状态,因为使用(new Form())
在原始单元测试线程上安装了WindowsFormsSynchronizationContext
的实例。使用语句在之后检查SynchronizationContext.Current
。然后,你面临着一个经典的僵局,斯蒂芬·克利里(Stephen Cleary)在他的演讲中很好地解释了这一点
是的,您不会阻止自己,但MSTest/NUnit会阻止,因为它足够聪明,可以识别异步任务的签名NeverCompletes
方法,然后执行类似于任务的操作。等待它返回的任务。因为原始的单元测试线程没有消息循环,也没有输出消息(不像windowsformsssynchronizationcontext
所期望的那样),所以Wait
内部的continuationNeverCompletes
永远没有机会执行任务。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
对象而设计的。如果这正是您所需要的,您可能需要查看我的messagelooppartment
,例如:
或者,如果您不关心测试的潜在耦合,您甚至可以跨多个单元测试方法使用它,例如(MSTest):
最后,无论是MessageLoopWorker
还是messageloopplant
都不是为处理在不同线程上创建的WinForms
对象而设计的(这几乎从来都不是一个好主意)。您可以拥有任意多个MessageLoopWorker
/messageloopfamilt
实例,但一旦在特定MessageLoopWorker
/messageloopfamilt
实例的线程上创建了WinForm
对象,应该在同一个线程上进一步访问和正确销毁它。令人惊讶的是,它非常有用。谢谢,我现在更明白了。我已经添加了置零上下文,希望它会很好。@AlexAtNet,如果有帮助,我会很高兴。我仍然不会在MessageLoopWorker
的线程之外创建任何WinForms对象。在大多数情况下,它们需要一个消息循环。至少,如果您这样做了,请确保将NUnit配置为给您一个STA线程。
[TestMethod]
public async Task NeverCompletes()
{
await MessageLoopWorker.Run(async (args) =>
{
using (new Form()) ;
return Type.Missing;
});
}
[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());
}
}
}
[TestClass]
public class MyTestClass
{
static MessageLoopApartment s_apartment;
[ClassInitialize]
public static void TestClassSetup()
{
s_apartment = new MessageLoopApartment();
}
[ClassCleanup]
public void TestClassCleanup()
{
s_apartment.Dispose();
}
// ...
}