如何正确地对相互依赖的方法进行单元测试?

7

考虑以下代码:

    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);
    }

正如您所看到的,AddComponent 添加到私有变量 _components。但是测试这一点的唯一方法是使用索引器。这很好,但为了测试索引器,我也必须调用 AddComponent
换句话说,在索引器和 AddComponent 的单元测试中,每个测试都必须调用这两种方法。这似乎创建了不必要的耦合。如果索引器中存在错误,则没有理由使我的 TestAddComponent 失败。
这里有什么最佳实践?我应该使用反射来获取 _components 吗?模拟?还是其他什么东西?
6个回答

7
在我看来,单元测试不应该进行反射以强制实现其目标。我认为在这种测试中,应该同时测试两者,在同一个测试中。但这只是一种观点。
然而,您可以进行多个测试,改变指令的顺序。尝试添加多个,并访问第一个,然后是最后一个,然后是中间的一个。每个测试都是一个场景,具有不同的顺序和插入数量。您甚至可以测试必须发生的异常状态...例如,如果您尝试获取未插入的内容。
我认为单元测试存在的目的是模拟使用或强制执行规范。不是为了检查程序的每一个细节是否正确,因为这会破坏灵活性。

7
你有两个选择:
1. 使用反射或MSTest私有访问器类在测试期间获取和设置私有字段值。 2. 不要担心它,测试公开的行为,即使这意味着您的测试取决于其他正在其他地方测试的属性或方法。
正如你从措辞中可以看出来一样,我的选择是第二个选项 - 你应该测试公开的行为。 在你的情况下,你要测试的公开行为是:
- 如果我使用AddComponent,则添加的组件应该可以通过索引器访问 - 如果我使用索引器,则应该能够访问通过AddComponent添加的任何组件
在这种情况下,很明显这几乎是相同的事情,所以我们只有一个单元测试/公开行为要测试。 是的,这个单元测试涵盖了两个不同的东西,但那并不重要 - 我们不是试图测试每个方法/属性的行为是否符合预期,而是想测试每个公开的行为是否按预期工作。
作为替代方案,假设我们选择选项1并使用私有反射自己检查_components的状态。 在这种情况下,我们实际上正在测试的行为是:
- 如果我使用AddComponent,则添加的组件应该添加到_components中 - 如果我使用索引器,则应该能够访问在_components中的任何组件
我们现在不仅测试类的内部行为(因此,如果实现发生更改,则测试会失败,即使类按预期工作),而且我们刚刚增加了正在编写的测试数量。 更重要的是,通过增加测试的复杂性,我们增加了测试本身存在错误的可能性 - 例如,如果我们犯了一个错误,在第二个测试中检查了一些完全不同的私有字段怎么办? 在这种情况下,我们不仅为自己增加了更多的工作量,而且甚至没有测试我们想要测试的实际行为!

2
当您使用Microsoft Unit Test Framework时,该框架会生成一个私有访问器类。这应该允许您访问您的私有类型。请参阅Microsoft的此页面以获取更多信息:http://msdn.microsoft.com/en-us/library/dd293546.aspx 特别是这一部分:创建可以访问内部、私有和友元方法的单元测试。

1
我可以建议使用接口、虚方法和MOQ。这样你就可以MOQ那些你不想测试的方法调用,并让它们返回你想要的结果。

1

这里有两个选项。

  1. 像已经提到的那样一起测试功能。
  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;
        }
    }
}


忘了提到另一种选项,那就是使用模拟的依赖注入。如果你要模拟 IDictionary,那么你可以验证你的测试。


public class MyClass
{    
    protected IDictionary _components;

    public MyClass()
    {
         _components = new Dictionary();
    }

    public MyClass(IDictionary 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);
    }
}

1
如果您想以这种方式实现,可以将代码移到构造函数之外(暂时忽略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);
    }
}

现在你可以模拟依赖并验证交互。

然而,考虑到缺乏附加行为,我认为真正实用的方法是直接公开成员并丢弃聚合对象的访问器:

class ComponentManager
{
    public Dictionary<Type, Component> Components { get; private set; }

    public ComponentManager()
    {
        Components = new Dictionary<Type, Component>();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接