2013-02-14 82 views
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测试。我们可以将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的构造函数,那么DoSomething将会失败。

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

那么,我的问题是如何让B的模拟跟着B的行为变化?

回答

1

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

因此,执行通过2条简单的规则,应当遵循在代码库:

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

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

+0

你的答案主要是我想的和我想要澄清的东西。看来你建议编写一套测试来验证Mocks实现的接口是否是例外的。这是一个好习惯吗?它是否引入了一些维护问题?看起来我们应该单元测试测试工具类呢?我们不应该在模拟中修复行为,而应该在单元测试中修复它(可能是每个接口实现应该被覆盖的抽象基本单元测试类,即所谓的合同测试)。我对吗? – gerichhome 2013-02-14 16:17:30

0

我与另一个答案不同,它似乎主张将真正的实现和(手工制作的)模拟都用于一组合约测试 - 它指定角色/接口的行为。我从来没有见过运动模拟测试 - 可以做到。

通常情况下,你不会手工模拟 - 而是使用模拟框架。所以w.r.t.你的榜样,我的客户测试将具有内嵌报表

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

你的问题是,当合同的变更,我该如何保证在客户端测试内嵌的预期也被更新(甚至标记)与更改依赖?

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

出路在于没有定义大量细粒度的IDependency接口。大多数问题都可以通过使用明确定义的非易失性行为的最小数量的块状角色(实现为接口)来解决。您可以尝试最小化角色级别的更改。最初这也是我的一个难点 - 然而discussions with interaction-test experts和实践经验已经设法赢得我。如果这种情况经常发生,那么对易变接口原因的快速回顾应该会产生更好的结果。