2

模拟对象引入了一种对某些程序单元进行深度行为测试的好方法。您只需将模拟的依赖项传递给被测单元并检查它是否像应做的那样与依赖项一起工作。

让你有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();
        }
    }
}

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

要对类 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 != some special value) 修改为 if(State != another value) )只有 TestB 失败。

这家伙修复了这个测试,并认为一切都恢复正常了。

但是,如果您尝试将 B 传递给 A 的构造函数,A.DoSomething 将会失败。

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

所以,我的问题是如何让 B 的模拟跟随 B 的行为变化

4

2 回答 2

1

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

因此,执行是通过代码库中应遵循的 2 个简单规则:

  1. 当一个类实现一个接口时,它必须在修改后实现接口的所有定义行为。
  2. 当您更改接口时,您必须调整所有实现者以实现新接口。

理想情况下,您有针对 IDependency 的已定义行为进行测试的单元测试,这些测试适用于 B 和 BMock 并捕获违反这些规则的行为。

于 2013-02-14T13:44:50.203 回答
0

我与另一个答案不同,它似乎提倡将真实实现和(手工制作的?)模拟都接受一组合同测试——这些测试指定了角色/界面的行为。我从来没有见过模拟练习的测试——尽管可以。

通常你不会手工制作模拟——而是使用模拟框架。所以在你的例子中,我的客户端测试会有内联语句

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

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

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

出路是不要鲁莽地定义很多细粒度的 IDependency 接口。大多数问题都可以通过最少数量的具有明确定义的非易失性行为的块状角色(实现为接口)来解决。您可以尝试最小化角色级别的更改。最初,这也是我的一个症结所在——但与交互测试专家的讨论和实践经验成功地赢得了我的支持。如果这种情况确实经常发生,那么快速回顾一下变化无常的界面的原因应该会产生更好的结果。

于 2013-02-18T06:53:09.973 回答