我对WebBrowser控件进行了一些测试,并用MessageLoopWorker包装,如下所述: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 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
});
}
如果运行测试时没有使同步上下文无效,则WebBrowserExtensionTest在遵循BasicControlTests时会阻塞。使用null可以顺利通过。
这样保留它可以吗?
最佳答案
我在MSTest下对此进行了重现,但我相信以下所有内容同样适用于NUnit。
首先,我知道该代码可能已脱离上下文,但就目前而言,它似乎不是很有用。为什么要在NeverCompletes
中创建一个窗体,该窗体在随机的MSTest / NUnit线程上运行,而不是MessageLoopWorker
生成的线程?
无论如何,您遇到了死锁,因为using (new Form())
在该原始单元测试线程上安装了WindowsFormsSynchronizationContext
的实例。在SynchronizationContext.Current
语句之后检查using
。然后,您将面临由Stephen Cleary在他的"Don't Block on Async Code"中解释的经典僵局。
是的,您不会阻塞自己,但是MSTest / NUnit会阻塞,因为它足够聪明,可以识别async Task
方法的NeverCompletes
签名,然后对它返回的Task.Wait
执行类似Task
的操作。因为原始的单元测试线程没有消息循环,也没有泵送消息(与WindowsFormsSynchronizationContext
期望的不同),所以await
中的NeverCompletes
延续永远不会有执行的机会,而Task.Wait
只是挂等待。
就是说,MessageLoopWorker
仅被设计为在传递给WinForms
的async
方法的范围内创建并运行MessageLoopWorker.Run
对象,然后完成操作。例如,以下内容不会被阻止:
[TestMethod]
public async Task NeverCompletes()
{
await MessageLoopWorker.Run(async (args) =>
{
using (new Form()) ;
return Type.Missing;
});
}
它不能与多个
WinForms
调用中的MessageLoopWorker.Run
对象一起使用。如果这是您需要的,则可能要查看here中的我的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());
}
}
}
或者,如果您不担心测试的潜在耦合,例如,您甚至可以跨多种单元测试方法使用它。 (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
实例,但是一旦在特定WinForm
/ MessageLoopWorker
实例的线程上创建了MessageLoopApartment
对象,则应进一步对其进行访问并进行适当销毁仅在同一线程上。