Xaml WPF 单元测试中的资源(如字符串 table)

Xaml resources (like a string table) in WPF unit testing

我的 WPF 项目中有一个 StringTable.xaml,带有 <system:String x:Key="IDS_DATA_HEADER_TIMED_TEST_RECORD_NUMBER">Record Number</system:String>。我的模型使用这个 StringTable,public static string foobar = (string)Application.Current.FindResource("PLACEHOLDER_TEXT"); 因此,我无法在 MSTest 中对我的模型进行单元测试,而它不知道 xaml。

这个问题填补了一个空白,因为关于 xaml 的许多问题都是如何与 GUI 结合的。是的,好的做法是使用 MVVM 模式将模型与 GUI 分开,并且只测试模型。是的,我的模型可能与 GUI 框架紧密耦合,因此我无法轻松地从 WPF 切换到其他模型。

在我的单元测试中,如果我尝试使用一个使用 StringTable.xaml 的函数,我注意到以下三个错误之一:

System.Windows.ResourceReferenceKeyNotFoundException: 'PLACEHOLDER_TEXT' resource not found.

Null pointer exception when I try to use the variable foobar

Casting / Conversion error when trying to cast the unfound resource to string with (string)Application.Current.FindResource("PLACEHOLDER_TEXT");

为澄清起见,StringTable.xaml 已作为合并词典添加到我的 App.xaml 文件中:

  <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="PlaceholderNamespace/SomeStyles.xaml"/>
                <ResourceDictionary Source="PlaceholderNamespace/NumberTable.xaml"/>
                <ResourceDictionary Source="PlaceholderNamespace/StringTable.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
  </Application.Resources>

我在类似的 Stack Overflow 中关注了 Wesley's post 并添加了(代码片段完全重复):

var app = new App(); //magically sets Application.Current
app.InitializeComponent(); //parses the app.xaml and loads the resources

到我的测试的顶部。这在 运行 一次进行一个单元测试时效果很好。但是,如果我尝试按顺序 运行 进行多个单元测试,则会收到错误消息:

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

因此,我必须运行每个单元测试。一。在。 A、时间。这很烦人,所以我要么开始将我所有的测试代码放入尽可能少的测试中(这会破坏目的),要么 运行 它们的频率低于我应有的频率,因为它需要很长时间才能通过我的测试套件(这违背了目的)。如果我只将它放在序列中第一个单元测试的顶部,我会注意到前面提到的错误。这意味着资源似乎在第一次测试后卸载了。但是 AppDomain 仍然存在。

我使用 MSTest,但已阅读 NUnit also suffers the same issue of not creating new app domains. Jeremy Wiebe's 对关于 MSTest 的 Stack Overflow 的回答提到创建新的 AppDomain 很昂贵。这将回答为什么它不会被多次创建,但不会回答我的情况如何解决这个问题。

有趣的是,我实际上看到第一个测试成功,而第二个测试正在处理。当两个测试都完成时,它们都将失败。就好像测试可以追溯失败一样,因为应用程序域试图更改。

有人知道我如何将所有 StringTable.xaml 资源加载到 Application.Current 中,并让它按顺序在所有单元测试中持续存在吗?

"Magic"方法一

类似于问题link处的Maslow's答案,您可以使用:

        App app = (App) Application.Current;
        if (app == null)
        {
            app = new App();
            app.InitializeComponent();
        }

在每个单元测试的顶部。这意味着只会创建和初始化一个 App。有趣的是,即使你在每次测试时从 if 语句中取出 app.InitializeComponent() 到 运行,它似乎有一个内部检查来防止相同的资源被多次初始化。

"Robust"方法二

我并不总能使魔术方法一奏效,但我无法找出它失败的原因。我创建了一个辅助方法。在每个单元测试中,它使用类似正则表达式的逻辑来解析您的 xaml 文件,并 Application.Current.Resources.Add(key, value) 将每个值加载到您的应用程序域中。

 [TestMethod]
    public async Task whenMethodTwoThenNoError()
    {
        // Arrange
        App app = (App) Application.Current;
        if (app == null)
        {
            app = new App();
        }
        string path = @"C:\Projects\software\PlaceholderProject\PlaceholderNamespace\StringTable.xaml";
        loadXamlResourcesFromFile("string", path);
        string path = @"C:\Projects\software\PlaceholderProject\PlaceholderNamespace\NumberTable.xaml";
        loadXamlResourcesFromFile("double", path);

        // Act
        // Nothing

        // Assert
        Assert.AreEqual(true, true);

    }


  public void loadXamlResourcesFromFile(string currentType, string path)
    {
        string line;
        try
        {
            using (StreamReader sr = new StreamReader(path))
            {
                line = sr.ReadLine();
                while (line != null)
                {
                    try
                    {
                        string keyPrefix = "system:String x:Key=\"";
                        string keySuffix = "\">";
                        string valueSuffix = "</system:String";

                        switch (currentType)
                        {
                            case "double":
                                keyPrefix = keyPrefix.Replace("String", "Double");
                                valueSuffix = valueSuffix.Replace("String", "Double");
                                break;
                            case "OtherType":
                                // TODO: replace text
                                break;
                        }

                        int keyPrefixLength = keyPrefix.Length;
                        int keySuffixLength = keySuffix.Length;


                        int indexBeginKey = line.IndexOf(keyPrefix) + keyPrefixLength;
                        int indexEndKey = line.IndexOf(keySuffix);
                        int indexBeginValue = line.IndexOf(keySuffix) + keySuffixLength;
                        int indexEndValue = line.IndexOf(valueSuffix);


                        if (indexEndValue < 0 && indexBeginKey >= 0) // If we see a key but not the end of a value...
                        { // I read in another line
                            line += sr.ReadLine();
                            indexBeginKey = line.IndexOf(keyPrefix) + keyPrefixLength;
                            indexEndKey = line.IndexOf(keySuffix);
                            indexBeginValue = line.IndexOf(keySuffix) + keySuffixLength;
                            indexEndValue = line.IndexOf(valueSuffix);
                        }

                        if (indexEndValue < 0 && indexBeginKey >= 0) // If we still do not see the end of a value...
                        { // I read in a third line
                            line += sr.ReadLine();
                            indexBeginKey = line.IndexOf(keyPrefix) + keyPrefixLength;
                            indexEndKey = line.IndexOf(keySuffix);
                            indexBeginValue = line.IndexOf(keySuffix) + keySuffixLength;
                            indexEndValue = line.IndexOf(valueSuffix);
                        }

                        // My string table entries are a maximum of three lines. Example:
                        //  < system:String x:Key = "NOTIFICATION_OF_ERROR_REGIONAL_SETTINGS_FAILED_TO_FIND" >
                        // Failed to find regional settings of the current computer. Setting to Invariant Culture. 
                        // Program will use a period (.) for decimal point. </ system:String >

                        int keyLength = indexEndKey - indexBeginKey;
                        int valueLength = indexEndValue - indexBeginValue;

                        string key = line.Substring(indexBeginKey, keyLength);
                        string value = line.Substring(indexBeginValue, valueLength);


                        switch (currentType)
                        {
                            // If this not present, StaticResource.cs may throw TypeInitializationException on line:
                            // public static double FALSE_DOUBLE = (double)Application.Current.FindResource("FALSE_DOUBLE");
                            case "string":
                                Application.Current.Resources.Add(key, value);
                                break;
                            case "double":
                                Application.Current.Resources.Add(key, double.Parse(value));
                                break;
                            case "OtherType":
                                // TODO: add resource
                                break;
                        }

                    }
                    catch (Exception e)
                    {
                        // This catches errors such as if the above code reads in boiler plate text
                        // ( < ResourceDictionary xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" ),
                        // blank lines, or lines with commentary to myself.

                        // Note: This will not catch commented out lines that have the correct syntax, i.e.,
                        // <!--<system:String x:Key="CALLOUT_CALCULATIONS_IN_PROGRESS">Running calculations...</system:String>-->

                    }
                    finally
                    {
                        line = sr.ReadLine(); // to prepare for the next while loop
                    }
                }
            }
        }
        catch (Exception e)
        {
            throw; // This exception is most likely because of a wrong file path 
        }
    }