在创建Form之前,带有Application循环的NUnit测试会挂起 [英] NUnit test with Application loop in it hangs when Form is created before it

查看:58
本文介绍了在创建Form之前,带有Application循环的NUnit测试会挂起的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我对使用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.Runasync方法范围内创建和运行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();
    }

    // ...
}

最后,MessageLoopWorkerMessageLoopApartment都不能与在不同线程上创建的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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆