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的行为改变?
答案 0 :(得分:1)
这是一个观点问题。通常,您模拟接口,而不是具体的类。在你的例子中,B的mock是IDependency的实现,就像B一样。只要IDependency的行为发生变化,B的模拟必须改变,并且当你改变IDependency的定义行为时,你可以通过查看IDependency的所有实现来确保。
因此,执行是通过代码库中应该遵循的两个简单规则来实现的:
理想情况下,您可以使用单元测试来测试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和实践经验已经成功赢得了我。如果这种情况经常发生,那么对变化无常接口的原因进行快速回顾应该会产生更好的结果。