如果我有课
public class OriginalThing
{
public void DoesThing(List<string> stuff)
{
var s = "Original Thing";
Console.WriteLine(s);
stuff.Add(s);
}
}
我可以通过说:
来测试它 [Test]
public void OriginalThing_DoesThing()
{
var strings= new List<string>();
new OriginalThing().DoesThing(strings);
strings.Should().NotBeEmpty();
strings.Should().HaveCount(1);
strings.ElementAt(0).Should().Contain("Original");
}
我也可以使用Moq将该对象提供给我:
public interface IThingServer
{
OriginalThing GetThing();
}
[Test]
public void ProvidedOriginalThing_DoesThing()
{
var strings= new List<string>();
var thingServer = new Mock<IThingServer>();
thingServer.Setup(ts => ts.GetThing()).Returns(new OriginalThing());
thingServer.Object.GetThing().DoesThing(strings);
strings.Should().NotBeEmpty();
strings.Should().HaveCount(1);
strings.ElementAt(0).Should().Contain("Original");
}
现在,OriginalThing不是从接口继承,也不是虚方法,所以我无法模拟它以验证与它的交互。
如果我继承了OriginalThing:
class FakeThing : OriginalThing
{
public void DoesThing(List<string> stuff)
{
var s = "Fake Thing";
Console.WriteLine(s);
stuff.Add(s);
}
}
然后我可以在测试中使用它而不调用原始方法:
[Test]
public void FakeThing_DoesThing()
{
var strings = new List<string>();
new FakeThing().DoesThing(strings);
strings.Should().NotBeEmpty();
strings.Should().HaveCount(1);
strings.ElementAt(0).Should().Contain("Fake");
}
如果我尝试让Moq提供FakeThing或使用新界面制作我自己的手册
public interface IThingFactory
{
OriginalThing GetThing();
}
public class FakeThingFactory : IThingFactory
{
public OriginalThing GetThing()
{
return new FakeThing();
}
}
public class OriginalThingFactory : IThingFactory
{
public OriginalThing GetThing()
{
return new OriginalThing();
}
}
如果我现在再添加两个测试
[Test]
public void ProvidedOriginalThing_DoesThing()
{
var strings = new List<string>();
var factory = new OriginalThingFactory();
var originalThing = factory.GetThing();
originalThing.Should().BeOfType<OriginalThing>();
originalThing.DoesThing(strings);
strings.Should().NotBeEmpty();
strings.Should().HaveCount(1);
strings.ElementAt(0).Should().Contain("Original");
}
[Test]
public void ProvidedFakeThing_DoesThing()
{
var strings = new List<string>();
var factory = new FakeThingFactory();
var fakeThing = factory.GetThing();
fakeThing.Should().BeOfType<FakeThing>();
fakeThing.DoesThing(strings);
strings.Should().NotBeEmpty();
strings.Should().HaveCount(1);
strings.ElementAt(0).Should().Contain("Fake");
}
然后,strings.ElementAt(0).Should().Contain("Fake");
行上的ProvidedFakeThing_DoesThing方法失败。这是因为即使它通过测试它是FakeThing
的实例,它实际上调用了基础OriginalThing.DoesThing
方法。
我可以投射fakeThing as FakeThing
并且测试将通过但是......好吧,在现实世界中,我们有一个方法,有三个职责,每个职责都要召集给一个合作者。我们想测试一下这三个电话,这个问题阻碍了我们。
想象一个拥有ThingFactory的ThingHandler,得到一个Thing,然后调用DoesThing我们想要控制Thing而不更改代码(也许这相当于棒上的月亮)
我们可能会尝试做一些愚蠢或糟糕的事情,但如果有办法实现这一目标,我们就无法看到它会很棒。
可以在https://github.com/pauldambra/methodHiding/
找到正在运行的示例提前感谢您的阅读!
答案 0 :(得分:3)
我建议阅读Growing Object-Oriented Software, Guided by Tests;事实证明,这是我们很多人遇到的问题。 您的测试试图告诉您某些事情,作为您代码的消费者。
最终,您希望使用behavior verification来确保SUT正在调用OriginalThing
依赖项。但是,您提到为OriginalThing
创建测试双精度并不简单,因为它没有实现接口。
所以你的测试告诉你两件事之一:
OriginalThing
实现了一个接口,情况会更容易; 或强> OriginalThing
您的SUT显然依赖于OriginalThing
。可以使用Role Interface来描述此依赖关系吗? SUT真的只需要一些能够执行DoesThing
的外部对象吗?如果是,那么您已经确定了seam in your code。
如果OriginalThing
是您对此类角色的唯一实施,该怎么办?为某些只有一个实现的东西定义一个接口是不是太过分了?好吧,您已经确定了另一种可能的实现 - 测试双重。
(测试只是代码的一个消费者。因此,如果测试认为有能力为角色指定特定实现,那么这种情绪可能会与其他消费者共享。)
现在,如果SUT真正依赖于OriginalThing
(而不仅仅是DoesThing
的任何旧对象),那么不应该被 替换它有一个测试双。在该场景中,测试应验证SUT是否正确处理由OriginalThing
填充的列表,而不是验证是否OriginalThing
被调用。
修改代码只是为了使其可测试并不是一件坏事。事实上,这是测试驱动开发背后的推动力。