C# 在窗体之前创建窗体时,应用程序循环在其中的NUnit测试将挂起

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()

我对使用MessageLoopWorker包装的WebBrowser控件进行了一些测试,如下所述:

但当另一个测试创建用户控件或窗体时,该测试将冻结且永远不会完成:

    [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
内部的continuation
NeverCompletes
永远没有机会执行任务。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();
    }

    // ...
}