如何在不改变方法签名的情况下返回Test Double ?
本文关键字:情况下 返回 Test Double 改变 方法 | 更新日期: 2023-09-27 18:01:51
如果我有一个class
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");
}
那么ProvidedFakeThing_DoesThing方法在strings.ElementAt(0).Should().Contain("Fake");
行失败。这是因为即使它通过了作为FakeThing
实例的测试,它实际上调用了基本的OriginalThing.DoesThing
方法。
我可以释放fakeThing as FakeThing
并且测试将通过,但是…在现实世界中,我们有一个方法,它有三个职责,每个职责都是调用一个合作者。我们想测试这三个调用是否已经完成,而这个问题正是阻碍我们的原因。
想象一个ThingHandler,它有一个ThingFactory,获取一个Thing,然后调用DoesThing,我们希望在不改变代码的情况下控制Thing(也许这相当于月球在棍子上)
我们可能试图做一些愚蠢或糟糕的事情,但如果有一种方法可以实现这一点,我们看不到,那将是很棒的。
一个运行的例子可以在https://github.com/pauldambra/methodHiding/
找到提前感谢阅读这么多!
我推荐阅读《成长中的面向对象软件,由测试引导》;事实证明,这是我们很多人都会遇到的问题。你的测试是试图告诉你一些事情,作为你的代码的消费者。
最终,您希望使用行为验证来确保SUT对OriginalThing
依赖项进行了正确的调用。但是,您提到为OriginalThing
创建测试双精度并不简单,因为它没有实现接口。
所以你的测试告诉你两件事中的一件:
- 如果
OriginalThing
实现一个接口,事情会更容易;或 如果您不测试SUT对
OriginalThing
的依赖,事情会更容易。您的SUT显然依赖于OriginalThing
。这个依赖关系可以用角色接口来描述吗?SUT真的只需要一些能够执行DoesThing
的外部对象吗?如果是这样,那么你已经确定了代码中的一个接缝。
如果OriginalThing
是这个角色的唯一实现怎么办?为只有一个实现的东西定义一个接口是不是有点过分了?嗯,您已经确定了另一种可能的实现——test double。
(测试只是代码的一个消费者。因此,如果测试认为为角色指定特定实现的能力是有用的,那么这种感觉很可能与其他消费者共享。
现在,如果SUT确实依赖于OriginalThing
(而不仅仅是任何可以DoesThing
的旧对象),那么不应该允许用测试双精度对象替换它。在该场景中,测试应该验证SUT是否正确处理由OriginalThing
填充的列表,而不是验证OriginalThing
是否被调用。
修改代码使其可测试并不是一件坏事。事实上,这就是测试驱动开发背后的推动力。