c# - 当在其之前创建 Form 时,带有应用程序循环的 NUnit 测试会挂起

标签 c# .net winforms async-await nunit

我对用 MessageLoopWorker 包装的 WebBrowser 控件进行了一些测试,如下所述: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 wait 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 之后会阻塞。通过归零,它可以很好地通过。

一直这样下去可以吗?

最佳答案

我在 MSTest 下重现了这一点,但我相信以下所有内容同样适用于 NUnit。

首先,我知道这段代码可能被断章取义,但就目前情况而言,它似乎并不是很有用。为什么要在 NeverCompletes 中创建一个表单,该表单在随机 MSTest/NUnit 线程上运行,与 MessageLoopWorker 生成的线程不同?

无论如何,您遇到了死锁,因为 using (new Form()) 会在原始单元测试线程上安装 WindowsFormsSynchronizationContext 的实例。检查 using 语句后的 SynchronizationContext.Current。然后,你面临着一个经典的僵局,Stephen Cleary 在他的 "Don't Block on Async Code" 中对此做了很好的解释。 .

是的,你不会阻止自己,但 MSTest/NUnit 会阻止自己,因为它足够聪明,可以识别 NeverCompletes 方法的 async Task 签名,然后执行类似 Task.Wait 位于其返回的 Task 上。由于原始单元测试线程没有消息循环并且不泵送消息(与 WindowsFormsSynchronizationContext 所期望的不同),因此 NeverCompletes< 内的 await 延续 永远没有机会执行,而 Task.Wait 只是挂着等待。

也就是说,MessageLoopWorker 仅设计用于在 async 方法范围内创建和运行 WinForms 对象> 您传递给 MessageLoopWorker.Run,然后完成。例如,以下内容不会被阻止:

[TestMethod]
public async Task NeverCompletes()
{
    await MessageLoopWorker.Run(async (args) =>
    {
        using (new Form()) ;
        return Type.Missing;
    });
}

设计用于跨多个 MessageLoopWorker.Run 调用使用 WinForms 对象。如果这正是您所需要的,您可能需要查看我的 MessageLoopApartment(来自 here) ,例如:

[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());
        }
    }
}

或者,如果您不担心测试的潜在耦合,您甚至可以在多个单元测试方法中使用它,例如(MS测试):

[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 实例,但是一旦在特定 的线程上创建了 WinForm 对象, >MessageLoopWorker/MessageLoopApartment 实例,它应该仅在同一线程上进一步访问和正确销毁。

关于c# - 当在其之前创建 Form 时,带有应用程序循环的 NUnit 测试会挂起,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41533997/

相关文章:

c# - 使 ListBox 项目具有与项目文本不同的值

c# - 如何为高级搜索屏幕构建自定义表达式

c# - C# Windows 窗体中的状态栏

c# - 按 id 对 List<T> 中的对象进行分组,并按每个对象的 duplicateCount 对列表进行排序

c# - 参数化 SQL 查询不返回结果,String 格式查询返回正确结果

c# - 如何使 switch 语句更面向对象?

c# - 尝试从 linq 正在创建的(匿名类)对象获取属性/字段

.net - 为什么不能在PowerShell中的事件处理程序中设置表单的DialogResult?

c# - 如何在 ListView 绑定(bind) Xamarin.Forms 中创建网格

c# - ASP.NET 线程问题