C# 如何正确地使用相互依赖的单元测试方法?

C# 如何正确地使用相互依赖的单元测试方法?,c#,unit-testing,C#,Unit Testing,考虑以下代码: private readonly Dictionary<Type, Component> _components = new Dictionary<Type, Component>(); public Component this [Type type] { get { Component component; _components.TryGetVa

考虑以下代码:

    private readonly Dictionary<Type, Component> _components = new Dictionary<Type, Component>();

    public Component this [Type type]
    {
        get
        {
            Component component;
            _components.TryGetValue(type, out component);
            return component;
        }
    }

    public void AddComponent(Component component)
    {
        _components.Add(component.GetType(), component);
    }
private readonly Dictionary\u components=new Dictionary();
公共组件此[类型]
{
得到
{
组分;
_组件。TryGetValue(类型,输出组件);
返回组件;
}
}
公共void AddComponent(组件组件)
{
_添加(component.GetType(),component);
}
如您所见,
AddComponent
添加到私有
\u components
变量中。但是测试这种情况的唯一方法是使用索引器。这很好,但是为了测试索引器,我也必须调用
AddComponent

换句话说,在索引器和
AddComponent
的单元测试中,每个测试都必须调用这两种方法。这似乎是在制造不必要的耦合。如果索引器中存在错误,则我的
TestAddComponent
没有理由失败


这里的最佳实践是什么?我是否使用反射来获取组件?嘲笑?还有别的吗?

在我看来,单元测试不应该通过反射来强制实现它的目标。我认为在这种测试中,两者应该在同一个测试中一起测试。但这只是一个观点

但是,您可以进行多个测试,更改指令的顺序。尝试添加多个,然后访问第一个,然后是最后一个,然后从中间添加一个。每个测试都是一个场景,具有不同的顺序和插入次数。您甚至可以测试必须发生的异常状态。。。例如,如果您试图获取未插入的内容


我认为,单元测试的存在是为了模拟使用,或者是为了执行规范。我不想看到程序的每一位是否都是正确的,因为这会扼杀灵活性。

我建议使用接口和/或虚拟方法以及MOQ。这样,您就可以停止对不想测试的方法的调用,并使它们返回您想要的结果。

当您使用Microsoft单元测试框架时,框架会生成一个私有访问器类。这应该允许您访问私有类型。有关更多信息,请查看Microsoft提供的此页面:


特别是本节:创建可以访问内部、私有和朋友方法的单元测试。

您有两个选项:

  • 在测试期间,使用反射或MSTest私有访问器类获取和设置私有字段值
  • 只是不用担心,测试暴露的行为,即使这意味着您的测试依赖于其他地方正在测试的其他属性或方法
  • 从措辞上你可能可以看出,我的选择是#2-你应该测试暴露的行为。在您的情况下,您正在测试的暴露行为是:

    • 如果我使用
      AddComponent
      ,那么添加的组件应该可以通过索引器访问
    • 如果我使用索引器,我应该能够访问通过
      AddComponent
    在这种情况下,很明显,它们几乎是一样的,所以我们这里只需要测试一个单元案例/暴露行为。是的,这个单元测试涵盖了两个不同的方面,但这并不重要——我们并不是要测试每个方法/属性的行为是否符合预期,而是要测试每个公开的行为是否符合预期


    作为替代方案,假设我们选择选项1并使用私有反射来检查
    \u组件的状态。在这种情况下,我们实际测试的时间是:

    • 如果我使用
      AddComponent
      ,则添加的组件应添加到
      \u components
    • 如果我使用索引器,我应该能够访问
      \u components
    我们现在不仅在测试类的内部行为(因此,如果实现发生变化,即使类按预期工作,测试也会失败),而且我们正在编写的测试数量刚刚增加了一倍


    最重要的是,通过增加我们测试的复杂性,我们增加了测试本身存在bug的可能性——例如,如果我们在测试2中犯了错误会怎么样。我们查了一些完全不同的私人领域?在这种情况下,我们不仅为自己做了更多的工作,而且我们甚至没有测试我们想要测试的实际行为

    这里有两种选择

  • 如前所述,一起测试功能
  • 修改类,以便可以将其包装在测试线束中
  • 测试线束应与单元测试一起定义,并公开验证功能是否正常工作所需的元素。您应该在单元测试中使用测试线束,而不是直接使用类

    
    public class MyClass
    {    
        protected readonly Dictionary<Type, Component> _components = new Dictionary<Type, Component>();
    
        public Component this [Type type]
        {
            get
            {
                Component component;
                _components.TryGetValue(type, out component);
                return component;
            }
        }
    
        public void AddComponent(Component component)
        {
            _components.Add(component.GetType(), component);
        }
    }
    
    public class MyClassTestHarness : MyClass
    {
        public Dictionary<Type, Component> Components
        {
            get
            {
                return _components;
            }
        }
    }
    
    

    如果您想这样做,请移出类进入构造函数(暂时忽略IoC框架),并使用接口引用依赖项:

    class ComponentManager
    {
        private readonly IDictionary<Type, Component> _components;
    
        public ComponentManager()
            : this(new Dictionary<Type, Component>())
        { }
    
        public ComponentManager(IDictionary<Type, Component> components)
        {
            _components = components;
        }
    
        public Component this[Type type]
        {
            get
            {
                Component component;
                _components.TryGetValue(type, out component);
                return component;
            }
        }
    
        public void AddComponent(Component component)
        {
            _components.Add(component.GetType(), component);
        }
    }
    
    类组件管理器
    {
    专用只读IDictionary组件;
    公共组件管理器()
    :此(新词典())
    { }
    公共组件管理器(IDictionary组件)
    {
    _组件=组件;
    }
    公共组件此[类型]
    {
    得到
    {
    组分;
    _组件。TryGetValue(类型,输出组件);
    返回组件;
    }
    }
    公共void AddComponent(组件组件)
    {
    _添加(component.GetType(),component);
    }
    }
    
    现在您可以模拟依赖关系并验证交互

    然而,由于缺乏额外的行为,我相信真正实用的方法是直接和通过
    class ComponentManager
    {
        private readonly IDictionary<Type, Component> _components;
    
        public ComponentManager()
            : this(new Dictionary<Type, Component>())
        { }
    
        public ComponentManager(IDictionary<Type, Component> components)
        {
            _components = components;
        }
    
        public Component this[Type type]
        {
            get
            {
                Component component;
                _components.TryGetValue(type, out component);
                return component;
            }
        }
    
        public void AddComponent(Component component)
        {
            _components.Add(component.GetType(), component);
        }
    }
    
    class ComponentManager
    {
        public Dictionary<Type, Component> Components { get; private set; }
    
        public ComponentManager()
        {
            Components = new Dictionary<Type, Component>();
        }
    }