在 STA Thread WPF 下运行多个 xunit 测试时出现问题

Posted

技术标签:

【中文标题】在 STA Thread WPF 下运行多个 xunit 测试时出现问题【英文标题】:Problems running multiple xunit tests under STA Thread WPF 【发布时间】:2021-08-11 07:56:21 【问题描述】:

在我对xunit wpf tests 的实验之后,我在运行多个测试时遇到了一个问题。

问题是当我在断言中检查 Application.Current.Windows 时。

密码

复制以下代码会导致问题:

测试窗口

<Window x:Class="acme.foonav.Issues.TestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:acme.foonav.Issues"
        mc:Ignorable="d"
        Title="TestWindow" Height="450" Width="800">
    <Grid>
    </Grid>
</Window>

测试

public class WpfFactIssues

    public WpfFactIssues()
    
        if (Application.Current == null)
        
            new Application();
        
    

    [WpfFact]
    public void Test1()
    
        TestWindow window = new TestWindow();
        
        Assert.Equal(typeof(TestWindow), Application.Current.Windows[0]?.GetType());
    
    
    [WpfFact]
    
    public void Test2()
    
        TestWindow window = new TestWindow();
        
        Assert.Equal(typeof(TestWindow), Application.Current.Windows[0]?.GetType());
    

所以在这里,Test1 和 Test2 是相同的。我已经删除了演示此场景所需的任何其他逻辑以专注于实际问题 - 而不是我为什么要这样做!

该场景的目的是检查一个窗口是否已添加到当前应用程序的窗口集合中。

我们正在使用Xunit.StaFact 来管理在 STA 线程上的运行。

问题

如果我执行所有测试(在 Rider 中),那么 Test1 将通过,Test2 将在断言上失败。

System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.

但是,我可以分别成功执行Test1Test2

执行时,Test1 将在线程 id (Thread.CurrentThread.ManagedThreadId) 为 20 时运行,然后Test2 将继续执行。

Test2 执行时,Application.Current 被设置为Test1 设置。

我尝试过的

实现IDisposable 并尝试调用Application.Current?.Shutdown() 以拼命尝试使其工作。

public void Dispose()

    if (Application.Current != null)
    
        ManualResetEventSlim manualResetEvent = new ManualResetEventSlim(false);
    
        Application.Current.Exit += (sender, args) =>  manualResetEvent.Set(); ;
        Application.Current?.Shutdown();

        manualResetEvent.Wait(TimeSpan.FromSeconds(5));     
    

这里永远不会引发 Exit 事件。

这将引发不同的异常:

System.InvalidOperationException: Cannot create more than one System.Windows.Application instance in the same AppDomain.

求助!

在同一个类中执行大量方法时,有没有办法在单元测试中使用Application

更新

目前正在查看:

Manage Application.Current while all tests are running in Test Project

【问题讨论】:

【参考方案1】:

我暂时把这个贴在这里,虽然它解决了这个问题,但它产生了另一个问题。

如果我们在测试类上实现一个 dispose 模式,我们可以强制关闭应用程序。这有点粗略。

public void Dispose()

    if (Application.Current != null)
    
        Application.Current.Shutdown(0);
        var result = Application.Current.Dispatcher.InvokeAsync(() =>  , DispatcherPriority.ContextIdle).Result;
    

但是,当第二次测试运行时,会抛出以下异常:

System.InvalidOperationException: Cannot create more than one System.Windows.Application instance in the same AppDomain.

不要在家里尝试这个!

因此,让我们在 Application 的内部进行反思,获得一些有趣的乐趣 - 让这项工作发挥作用 - 至少在这种情况下:

让我们围绕应用跟踪我们的测试状态:

public static class FooYou

    public static bool hasInited = false;

当我们在测试构造函数中新建应用程序时,这一切都变得清晰了:

public WpfFactIssues()

    if (Application.Current == null)
    
        new Application()
        
            ShutdownMode = ShutdownMode.OnExplicitShutdown
        ;

        if (!FooYou.hasInited)
        
            FooYou.hasInited = true;
        
        else
        
            var appInit = typeof(Application).GetMethod("ApplicationInit", BindingFlags.Static | BindingFlags.NonPublic);
            appInit.Invoke(null, null);    
        
    

在 dispose 实现中的最后一个更改 - 假设应用程序不是在此应用程序域中创建的,否则这会导致 Cannot create more than one System.Windows.Application instance in the same AppDomain 异常。

public void Dispose()

    if (Application.Current != null)
    
        Application.Current.Resources.MergedDictionaries.Clear();
        
        Application.Current.Shutdown(0);
        var result = Application.Current.Dispatcher.InvokeAsync(() =>  , DispatcherPriority.ContextIdle).Result;

        var appCreated = typeof(Application).GetField("_appCreatedInThisAppDomain", 
            BindingFlags.Static | 
            BindingFlags.NonPublic);
        appCreated.SetValue(null, false);
    

这里我们强制重新初始化应用程序 - 正如 Application 的静态初始化程序所做的那样。

当然,这是一个令人憎恶的解决方案。这个不干净。它当然很容易被破坏 - 永远不要接触私人课程。

一个更可行的助手应用程序

以上内容有点粗略,所以总结一下,让测试工作起来不费吹灰之力:

using System.Reflection;
using System.Windows;
using System.Windows.Threading;

namespace acme.foonav.Fixture

    public static class ApplicationState
    
        private static bool hasApplicationPreviouslyInitialized;

        public static void CreateNew()
        
            Shutdown();
            if (Application.Current == null)
            
                new Application()
                
                    ShutdownMode = ShutdownMode.OnExplicitShutdown
                ;

                if (!hasApplicationPreviouslyInitialized)
                
                    hasApplicationPreviouslyInitialized = true;
                
                else
                
                    var pre = typeof(Application);
                    var clear = pre.GetMethod("ApplicationInit", BindingFlags.Static | BindingFlags.NonPublic);
                    clear.Invoke(null, null);    
                
            
        

        public static void Shutdown()
        
            if (Application.Current != null)
            
                Application.Current.Resources.MergedDictionaries.Clear();
                
                Application.Current.Shutdown(0);
                var result = Application.Current.Dispatcher.InvokeAsync(() =>  , DispatcherPriority.ContextIdle).Result;

                var appCreated = typeof(Application).GetField("_appCreatedInThisAppDomain", 
                    BindingFlags.Static | 
                    BindingFlags.NonPublic);
                appCreated.SetValue(null, false);
                
            
        
    


需要使用Application的测试基类:

using System;
using System.Threading.Tasks;
using System.Windows.Threading;
using acme.foonav.Fixture;

namespace acme.foonav

    public abstract class ApplicationContextTest : IDisposable
    
        protected ApplicationContextTest() => ApplicationState.CreateNew();

        public void Dispose() => ApplicationState.Shutdown();

        protected async Task AwaitDispatcher() => await Dispatcher.CurrentDispatcher.InvokeAsync(() =>  , DispatcherPriority.ContextIdle);
    


一个测试文件:

using System.Windows;
using Xunit;

namespace acme.foonav.Issues

    public class WpfFactIssues : ApplicationContextTest
    
        [WpfFact]
        public void Test1()
        
            TestWindow window = new TestWindow();
        
            Assert.Equal(typeof(TestWindow), Application.Current.Windows[0]?.GetType());
        
    
        [WpfFact]
        public void Test2()
        
            TestWindow window = new TestWindow();
        
            Assert.Equal(typeof(TestWindow), Application.Current.Windows[0]?.GetType());
        
    


并确保测试不会并行执行:

using Xunit;

[assembly: CollectionBehavior(DisableTestParallelization = true)]

【讨论】:

以上是关于在 STA Thread WPF 下运行多个 xunit 测试时出现问题的主要内容,如果未能解决你的问题,请参考以下文章

WPF 之 调用线程必须为 STA,因为许多 UI 组件都需要

如何使用 net core / net5+ 为 wpf 设置 STA 公寓状态? [复制]

如何在不同的线程上使用静态资源?

在 STA 下运行 NUnit 测试的问题

多个线程可以设置 STA 单元吗?

2021-12-12 WPF面试题 描述下WPF对象完整的层次结构?