我有一个带有静态构造函数的类,我用它来读取 app.config 值。如何使用不同的配置值对类进行单元测试。我正在考虑在不同的应用程序域中运行每个测试,这样我就可以为每个测试执行静态构造函数 - 但我这里有两个问题:
1.我不知道如何在单独的应用程序域中运行每个测试并且
2. 如何在运行时更改配置设置?
有人可以帮我吗?或者谁有更好的解决方案?谢谢。
就我个人而言,我会将静态构造函数粘贴到静态方法中,然后在静态块中执行该方法。
您不需要测试 .Net 是否能够从配置文件加载数据。
相反,尝试集中精力测试你自己的逻辑。
更改您的类,以便它从其构造函数(或通过属性)获取配置值,然后像使用任何其他依赖项一样对其进行测试。
一路走来,您的班级也朝着SRP前进。
根据配置加载 - 将此逻辑集中在一个单独的非静态类中。
编辑:
将配置逻辑分离到另一个类中。像这样的东西:
public static class ConfigurationLoader
{
static ConfigurationLoader()
{
// Dependency1 = LoadFromConfiguration();
// Dependency2 = LoadFromConfiguration();
}
public static int Dependency1 { get; private set; }
public static string Dependency2 { get; private set; }
}
然后,当您实例化您的类时,将其注入依赖项:
public class MyClass
{
private readonly int m_Dependency1;
private readonly string m_Dependency2;
public MyClass(int dependency1, string dependency2)
{
m_Dependency1 = dependency1;
m_Dependency2 = dependency2;
}
public char MethodUnderTest()
{
if (m_Dependency1 > 42)
{
return m_Dependency2[0];
}
return ' ';
}
}
public class MyClassTests
{
[Fact]
public void MethodUnderTest_dependency1is43AndDependency2isTest_ReturnsT()
{
var underTest = new MyClass(43, "Test");
var result = underTest.MethodUnderTest();
Assert.Equal('T', result);
}
}
...
var myClass = new MyClass(ConfigurationLoader.Dependency1, ConfigurationLoader.Dependency2);
您可以继续使用 IOC 容器,但是使用不同输入测试 MyClass 的问题可以通过这个简单的可测试设计解决。
如果您从
(Web)ConfigurationManager.AppSettings
读取,那只是一个 NameValueCollection,因此您可以将直接读取 ConfigurationManager.AppSettings
的代码替换为从任何 NameValueCollection 读取的代码。
只需将实际配置解析从静态构造函数中移出到静态方法即可。静态构造函数调用该静态方法并传递
ConfigurationManager.AppSettings
,但您可以从测试代码中调用该解析器方法,并验证配置解析,而无需实际接触文件或弄乱应用程序域。
但从长远来看,真正按照seldary的建议注入你的配置参数。创建一个配置类,在应用程序启动时读取实际值,并设置 IoC 容器以向所有请求者提供相同的配置实例。
这也使进一步的测试变得更容易,因为您的类不会从全局静态配置实例中读取。您可以只传递特定的配置实例来进行不同的测试。当然,为您的测试创建一个工厂方法,以构建全局配置,这样您就不必一直手动执行...
我最近也遇到了同样的问题。唯一的区别是配置值来自数据库而不是 app.config。我能够使用 TypeInitializer 解决它。
[Test]
public void TestConfigurationInStaticConstructor()
{
// setup configuraton to test
// ...
// init static constructor
ReaderTypeInit();
// Assert configuration effect
// ...
// reset static ctor to prevent other existing tests (that may depend on original static ctor) fail
ReaderTypeInit();
}
// helper method
private void ReaderTypeInit()
{
typeof(< your class with static ctor>).TypeInitializer.Invoke(null, new object[0]);
}
我遇到了类似的问题,因为我想检查我的应用程序如何使用 app.config 中保存的不同或缺失值工作。对互联网和 Stckoverflow 的大量搜索导致我创建了这个:
我在其中保存/访问设置的集中式应用程序设置示例。另外,我想使用从我的 app.config 文件中读取的各种设置来测试静态类。
public class ApplicationSettings
{
static ApplicationSettings()
{
ApplicationName = Settings.Default.ApplicationName;
ConnectionString = GetConnectionString("TestingDbConnection");
}
public static String ApplicationName { get; }
public static String ConnectionString { get; }
/// <summary>
/// Gets the connection string.
/// </summary>
/// <param name="dataConnectionName">Name of the data connection.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">dataConnectionName</exception>
public static String GetConnectionString(String dataConnectionName)
{
ConnectionStringSettings connectionStringSettings = ConfigurationManager.ConnectionStrings[dataConnectionName];
if (connectionStringSettings == null)
{
String errorMessage = $"Cannot load Connection named '{dataConnectionName}'. Check to make sure the connection is defined in the Configuration File.";
throw new ArgumentNullException(nameof(dataConnectionName), errorMessage);
}
String retVal = connectionStringSettings.ConnectionString;
return retVal;
}
}
为了在单元测试期间切换 app.config 文件,我创建了此类 AppConfigModifier
/// <summary>
/// The App Config Modifier class
/// </summary>
public class AppConfigModifier : IDisposable
{
/// <summary>
/// Initialises a new instance of the <see cref="AppConfigModifier"/> class.
/// </summary>
/// <param name="appConfigFile">The application configuration file.</param>
public AppConfigModifier(String appConfigFile)
{
OldConfig = AppDomain.CurrentDomain.GetData("APP_CONFIG_FILE").ToString();
TargetDomain = AppDomain.CreateDomain("UnitTesting", null, AppDomain.CurrentDomain.SetupInformation);
FileInfo fi = new FileInfo(OldConfig);
String newConfig = Path.Combine(fi.DirectoryName, appConfigFile);
TargetDomain.SetData("APP_CONFIG_FILE", newConfig);
ResetConfigMechanism();
}
/// <summary>
/// Gets or sets a value indicating whether [disposed value].
/// </summary>
/// <value>
/// <c>true</c> if [disposed value]; otherwise, <c>false</c>.
/// </value>
private Boolean DisposedValue { get; set; }
/// <summary>
/// Gets or sets the old configuration.
/// </summary>
/// <value>
/// The old configuration.
/// </value>
private String OldConfig { get; }
/// <summary>
/// Gets the target domain.
/// </summary>
/// <value>
/// The target domain.
/// </value>
public AppDomain TargetDomain { get; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
if (!DisposedValue)
{
TargetDomain.SetData("APP_CONFIG_FILE", OldConfig);
ResetConfigMechanism();
AppDomain.Unload(TargetDomain);
DisposedValue = true;
}
GC.SuppressFinalize(this);
}
/// <summary>
/// Resets the configuration mechanism.
/// </summary>
private static void ResetConfigMechanism()
{
Type configurationManagerType = typeof(ConfigurationManager);
FieldInfo initStateFieldInfo = configurationManagerType.GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static);
if (initStateFieldInfo != null)
{
initStateFieldInfo.SetValue(null, 0);
}
FieldInfo configSystemFieldInfo = configurationManagerType.GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static);
if (configSystemFieldInfo != null)
{
configSystemFieldInfo.SetValue(null, null);
}
Assembly currentAssembly = configurationManagerType.Assembly;
Type[] allTypes = currentAssembly.GetTypes();
Type clientConfigPathsType = allTypes.First(x => x.FullName == "System.Configuration.ClientConfigPaths");
FieldInfo currentFieldInfo = clientConfigPathsType.GetField("s_current", BindingFlags.NonPublic | BindingFlags.Static);
if (currentFieldInfo != null)
{
currentFieldInfo.SetValue(null, null);
}
}
}
出于我的目的,如果 DatabaseConnection 丢失,我希望它抛出一个很好的详细异常(ArgumentNullException 是我们认为符合要求的东西)。下面的测试用例确保会发生这种情况:
[TestClass]
[DeploymentItem("NoConnectionStringConfig.App.config")]
public class AppConfigSwitchTests
{
[TestMethod]
public void Test_MissingDatabaseConnection()
{
using (AppConfigModifier newDomain = new AppConfigModifier("NoConnectionStringConfig.App.config"))
{
String connectionName = "TestingDbConnection";
String parameterName = "dataConnectionName";
String expectedMessage1 = $"Cannot load Connection named '{connectionName}'. Check to make sure the connection is defined in the Configuration File.\r\nParameter name: {parameterName}";
Exception actualException = null;
try
{
String assemblyName = typeof(ApplicationSettings).Assembly.GetName().Name;
String typeName = typeof(ApplicationSettings).FullName;
_ = newDomain.TargetDomain.CreateInstanceAndUnwrap(assemblyName, typeName);
ApplicationSettings.GetConnectionString(connectionName);
}
catch (Exception exception)
{
actualException = exception;
}
Assert.IsNotNull(actualException);
Assert.IsInstanceOfType(actualException, typeof(TargetInvocationException));
Exception innerException = actualException.InnerException;
Assert.IsInstanceOfType(innerException, typeof(TypeInitializationException));
Exception raisedException = innerException.InnerException;
Assert.IsInstanceOfType(raisedException, typeof(ArgumentNullException));
ArgumentNullException anException = raisedException as ArgumentNullException;
String actualMessage1 = anException.Message;
Assert.AreEqual(expectedMessage1, actualMessage1);
String actualParameterName = anException.ParamName;
Assert.AreEqual(parameterName, actualParameterName);
}
}
}
最后一部分是加载到位的 app.config 文件的内容,方便地称为 NoConnectionStringConfig.App.config。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
</startup>
<connectionStrings>
<clear/>
<add name="UnitTesting" providerName="System.Data.SqlClient" connectionString="Server=TheServer;Database=UnitTesting;User Id=TheUserId;Password=ThePassword;"/>
</connectionStrings>
</configuration>