可维护的嘲笑

时间:2013-02-14 13:38:12

标签: unit-testing mocking maintainability

Mock对象引入了一种很好的方法来对某些程序单元进行深度行为测试。 您应该将模拟的依赖项传递给已测试的单元,并检查它是否与依赖项一致。

让你有2个A和B类:

public class A
{
    private B b;

    public A(B b)
    {
        this.b = b;
    }

    public void DoSomething()
    {
        b.PerformSomeAction();
        if(b.State == some special value)
        {
            b.PerformAnotherAction();
        }
    }
}


public class B
{
    public BState State { get; private set; }

    public void PerformSomeAction()
    {
        //some actions

        State = some special value;
    }

    public void PerformAnotherAction()
    {
        if(State != some special value)
        {
           fail();  //for example throw new InvalidOperationException();
        }
    }
}

想象一下,正在使用单元测试TestB测试B类。

对于单元测试类A,我们可以将B传递给它的构造函数(进行基于状态的测试)或将B的模拟传递给它(进行基于行为的测试)。

假设我们选择了第二种方法(例如我们不能直接验证A的状态,并且可以间接地进行)并创建单元测试TestA(不包含对B的任何引用)。

因此,我们将介绍一个接口IDependency,类将如下所示:

public interface IDependency
{
    void PerformSomeAction();
    void PerformAnotherAction();
}

public class A
{
    private IDependency d;

    public A(IDependency d)
    {
        this.d = d;
    }

    public void DoSomething()
    {
        d.PerformSomeAction();
        if(d.State == some special value)
        {
            d.PerformAnotherAction();
        }
    }
}


public class B : IDependency
{
    public BState State { get; private set; }

    public void PerformSomeAction()
    {
        //some actions

        State = some special value;
    }

    public void PerformAnotherAction()
    {
        if(State != some special value)
        {
           fail();  //for example throw new InvalidOperationException();
        }
    }
}

和单元测试TestB类似于:

[TestClass]
public class TestB
{
    [TestMethod]
    public void ShouldPerformAnotherActionWhenDependencyReturnsSomeSpecialValue()
    {
        var d = CreateDependencyMockSuchThatItReturnsSomeSpecialValue();
        var a = CreateA(d.Object);

        a.DoSomething();

        AssertSomeActionWasPerformedForDependency(d);
    }

    [TestMethod]
    public void ShouldNotPerformAnotherActionWhenDependencyReturnsSomeNormalValue()
    {
        var d = CreateDependencyMockSuchThatItReturnsSomeNormalValue();
        var a = CreateA(d.Object);

        a.DoSomething();

        AssertSomeActionWasNotPerformedForDependency(d);
    }
}

确定。这对开发人员来说是一个快乐的时刻 - 一切都经过测试,所有测试都是绿色的。一切都很好。

但是!

当某人修改了B类逻辑时(例如将if(State!=某些特殊值)修改为if(State!=另一个值)),只有TestB失败。

这家伙修复了这个测试并认为一切顺利。

但是如果你试图将B传递给A的构造函数。那么东西就会失败。

它的根本原因是我们的模拟对象。它修复了B对象的旧行为。当B改变其行为时,模拟没有反映出来。

所以,我的问题是如何模仿B跟随B的行为改变

2 个答案:

答案 0 :(得分:1)

这是一个观点问题。通常,您模拟接口,而不是具体的类。在你的例子中,B的mock是IDependency的实现,就像B一样。只要IDependency的行为发生变化,B的模拟必须改变,并且当你改变IDependency的定义行为时,你可以通过查看IDependency的所有实现来确保。

因此,执行是通过代码库中应该遵循的两个简单规则来实现的:

  1. 当类实现接口时,它必须在修改后满足接口的所有已定义行为。
  2. 更改界面时,必须调整所有实施者以完成新界面。
  3. 理想情况下,您可以使用单元测试来测试IDependency的已定义行为,该行为适用于B和BMock并捕获违反这些规则的行为。

答案 1 :(得分:0)

我与其他答案不同,它似乎主张将实际实现和(手工制作?)模拟同时置于一组合同测试中 - 它们指定了角色/接口的行为。我从来没有见过运动模拟的测试 - 虽然可以做到。

通常你不会手工制作模拟 - 而是使用模拟框架。所以w.r.t.例如,我的客户端测试将内联语句作为

new Mock<IDependency>().Setup(d => d.Method(params)
                       .Returns(expectedValue)

您的问题是合同何时更改,我如何保证客户端测试中的内联期望也会随着依赖项的更改而更新(或甚至标记)?

编译器在这里没有帮助。测试也不会。你所拥有的是客户端和依赖关系之间缺乏共享协议。您必须手动查找和替换(或使用IDE工具来查找对接口方法的所有引用)并进行修复。

出路是不要肆无忌惮地定义许多细粒度的IDependency接口。大多数问题可以用最少数量的粗略角色(实现为接口)来解决,并且具有明确定义的非易失性行为。您可以尝试最小化角色级别的更改。最初这对我来说也是一个棘手的问题 - 然而discussions with interaction-test experts和实践经验已经成功赢得了我。如果这种情况经常发生,那么对变化无常接口的原因进行快速回顾应该会产生更好的结果。