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
}
}
我的 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
}
}