如何在单元测试期间重写被测试类中的某些属性

本文关键字:测试类 属性 重写 单元测试 | 更新日期: 2023-09-27 17:54:39

我有一个类,我想用单元测试来测试它。它有一些逻辑来在本地xml文件中查找一些值,如果找不到该值,它将读取一些外部源(SqlServerDB(。但是在单元测试期间,我根本不希望这个组件与SqlServer交互。在单元测试期间,我想将外部源阅读器的SqlServer实现替换为一些Noop阅读器。实现这一目标的好方法是什么?重要提示:我不能定义接受读取器类型实例的构造函数,因为它是面向客户端的API,我想限制它们对我的类所能做的事情。我目前在单元测试中使用几种方法:

  • 使用反射将私有/受保护属性的值设置为我模拟的阅读器实现
  • 定义将创建具体类的工厂。在Unity container&被测试的类将根据我的需要从DI容器中获取工厂对象并实例化读取器
  • 正在测试的子类并在那里设置属性

但对我来说,它们似乎都不够干净。有什么更好的方法可以做到这一点吗?以下是演示测试中类示例的示例代码:

namespace UnitTestProject1
{
    using System.Collections.Generic;
    using System.Data.SqlClient;
    public class SomeDataReader
    {
        private Dictionary<string, string> store = new Dictionary<string, string>();
        // I need to override what this property does
        private readonly IExternalStoreReader ExternalStore = new SqlExternalStoreReader(null, false, new List<string>() {"blah"});

        public string Read(string arg1, int arg2, bool arg3)
        {
            if (!store.ContainsKey(arg1))
            {
                return ExternalStore.ReadSource().ToString();
            }
            return null;
        }
    }
    internal interface IExternalStoreReader
    {
        object ReadSource();
    }
    // This 
    internal class SqlExternalStoreReader : IExternalStoreReader
    {
        public SqlExternalStoreReader(object arg1, bool arg2, List<string> arg3)
        {
        }
        public object ReadSource()
        {
            using (var db = new SqlConnection("."))
            {
                return new object();    
            }
        }
    }
    internal class NoOpExternalStoreReader : IExternalStoreReader
    {
        public object ReadSource()
        {
            return null;
        }
    }
}
[TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            var objectToTest = new SomeDataReader();
            objectToTest.Read("", -5, false); // This will try to read DB, I don't want that.
        }
    }

如何在单元测试期间重写被测试类中的某些属性

在这种情况下,您可以使用InternalsVisibleTo属性,它将所有内部成员公开给友元程序集。

您可以从创建一个单独的internal构造函数重载开始,该重载可以接受IExternalStoreReader:的不同实例

public class SomeDataReader
{
    // publicly visible
    public SomeDataReader()
        : this(new SqlExternalStoreReader(...))
    { }
    // internally visible
    internal SomeDataReader(IExternalStoreReader storeReader)
    {
        ExternalStore = storeReader;
    }
    ...
}

然后,通过向AssemblyInfo.cs:添加InternalsVisibleTo属性,允许单元测试程序集访问内部成员

[assembly: InternalsVisibleTo("YourUnitTestingAssembly")]

如果你真的担心有人试图访问你的内部成员,你也可以使用带有InternalsVisibleTo属性的强命名,以确保没有人试图模拟你的单元测试程序集。

简单的答案是:更改代码。因为你有一个私人只读字段,它可以创造自己的价值,所以你不能在不变得非常愤怒的情况下改变这种行为。所以,不要那样做。将代码更改为:

public class SomeDataReader
{
    private Dictionary<string, string> store = new Dictionary<string, string>();
    private readonly IExternalStoreReader externalStore;
    public SomeDataReader(IExternalStoreReader externalStore)
    {
        this.externalStore = externalStore;
    }

换句话说,将IExternalStoreReader实例注入到类中。通过这种方式,您可以为单元测试创建noop版本,并为生产代码创建真正的版本。

如果您自己编译完整的代码,您可以在单元测试项目中创建一个带有存根实现的伪SqlExternalStoreReader。这个存根实现允许您访问所有字段并将调用转发到模拟框架/单元测试