0

I have a StringTable.xaml in my WPF project, with <system:String x:Key="IDS_DATA_HEADER_TIMED_TEST_RECORD_NUMBER">Record Number</system:String>. My model makes use of this StringTable, with public static string foobar = (string)Application.Current.FindResource("PLACEHOLDER_TEXT"); Thus, I cannot Unit Test my model in MSTest without it being aware of xaml.

This question fills a niche because many questions about xaml are how to unit to the GUI. Yes, good practice is to use the MVVM pattern to separate model from GUI, and only test the model. Yes, I probably have the model too tightly coupled to the GUI framework, and so I will not be able to easily switch from WPF to some other.

In my unit tests, if I try to use a function which uses StringTable.xaml, I noticed any one of three errors:

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

or

Null pointer exception when I try to use the variable foobar

or

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

For clarification, StringTable.xaml is added as a merged dictionary within my App.xaml file:

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

I have followed Wesley's post in a similar Stack Overflow and added (code snippet exactly duplicated):

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

to the top of my tests. This works well when running one unit test at a time. If I try to run multiple unit tests sequentially, however, I recieve the error:

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

Thus, I have to run each unit test. One. At. A. Time. This is annoying, so I will either start shoving all of my testing code into as few tests as possible (which kind of defeats the purpose), or run them less frequently than I should because it takes so long to get thru my test suite (which kind of defeats the purpose). If I only have it at the top of the first unit test in a sequence, I noticed the previously stated errors. This implies that the resources seemed to unload after the first test. But the AppDomain is still there.

I use MSTest, but have read that NUnit also suffers the same issue of not creating new app domains. Jeremy Wiebe's answer to a Stack Overflow about MSTest mentions that creating a new AppDomain is expensive. This would answer why it is not created more than once, but not how to get around this for my case.

Interestingly, I actually see the first test succeed while the second one is processing. When both tests complete, they will both fail. It's as if a test can retroactively fail because the app domain tried to change.

Anyone know how I can load all the StringTable.xaml resources into Application.Current, and have it persist through all unit tests in a sequence?

John Doe
  • 147
  • 3
  • 15
  • 1
    Have you tried assemblyinitialize and classinitialize attributes? https://stackoverflow.com/questions/6193744/what-would-be-an-alternate-to-teardown-and-setup-in-mstest/21304674#21304674 – Andy Jan 23 '19 at 19:39
  • Thanks much. Attributes (other than [TestMethod]) are new to me. I'll look into those this coming week. – John Doe Jan 23 '19 at 21:46
  • Are you saying to use [ClassInitialize] with the Wesley's post code snippet? If so, that worked. If you post that as an answer (writing out the full code and explanation for future viewers), I will accept. – John Doe Jan 30 '19 at 15:32

1 Answers1

0

"Magic" Method One

Similar to Maslow's answer at the link in the question, you can use:

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

at the top of each unit test. This means that only one App will be created and initialized. Interestingly, even if you take app.InitializeComponent() out of the if statement to run on every test, it seems to have an internal check to prevent the same resources from initializing multiple times.

"Robust" Method Two

I did not always get the Magic Method One to work, but I could not track down why it was failing to a consistent reason. I created a helper method. On each unit test, it uses regex-like logic to parse your xaml file, and Application.Current.Resources.Add(key, value) to load each value into your app domain.

 [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 
        }
    }
John Doe
  • 147
  • 3
  • 15