具有不同配置值的单元测试静态构造函数

问题描述 投票:0回答:5

我有一个带有静态构造函数的类,我用它来读取 app.config 值。如何使用不同的配置值对类进行单元测试。我正在考虑在不同的应用程序域中运行每个测试,这样我就可以为每个测试执行静态构造函数 - 但我这里有两个问题:
1.我不知道如何在单独的应用程序域中运行每个测试并且
2. 如何在运行时更改配置设置?

有人可以帮我吗?或者谁有更好的解决方案?谢谢。

c# unit-testing static-constructor
5个回答
0
投票

就我个人而言,我会将静态构造函数粘贴到静态方法中,然后在静态块中执行该方法。


0
投票

您不需要测试 .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 的问题可以通过这个简单的可测试设计解决。


0
投票

如果您从

(Web)ConfigurationManager.AppSettings
读取,那只是一个 NameValueCollection,因此您可以将直接读取
ConfigurationManager.AppSettings
的代码替换为从任何 NameValueCollection 读取的代码。

只需将实际配置解析从静态构造函数中移出到静态方法即可。静态构造函数调用该静态方法并传递

ConfigurationManager.AppSettings
,但您可以从测试代码中调用该解析器方法,并验证配置解析,而无需实际接触文件或弄乱应用程序域。

但从长远来看,真正按照seldary的建议注入你的配置参数。创建一个配置类,在应用程序启动时读取实际值,并设置 IoC 容器以向所有请求者提供相同的配置实例。

这也使进一步的测试变得更容易,因为您的类不会从全局静态配置实例中读取。您可以只传递特定的配置实例来进行不同的测试。当然,为您的测试创建一个工厂方法,以构建全局配置,这样您就不必一直手动执行...


0
投票

我最近也遇到了同样的问题。唯一的区别是配置值来自数据库而不是 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]);
}

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>
© www.soinside.com 2019 - 2024. All rights reserved.