如何在不改变方法签名的情况下返回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/

找到

提前感谢阅读这么多!

如何在不改变方法签名的情况下返回Test Double ?

我推荐阅读《成长中的面向对象软件,由测试引导》;事实证明,这是我们很多人都会遇到的问题。你的测试是试图告诉你一些事情,作为你的代码的消费者。

最终,您希望使用行为验证来确保SUT对OriginalThing依赖项进行了正确的调用。但是,您提到为OriginalThing创建测试双精度并不简单,因为它没有实现接口。

所以你的测试告诉你两件事中的一件:

  1. 如果OriginalThing实现一个接口,事情会更容易;
  2. 如果您不测试SUT对OriginalThing的依赖,事情会更容易。

您的SUT显然依赖于OriginalThing。这个依赖关系可以用角色接口来描述吗?SUT真的只需要一些能够执行DoesThing的外部对象吗?如果是这样,那么你已经确定了代码中的一个接缝。

如果OriginalThing是这个角色的唯一实现怎么办?为只有一个实现的东西定义一个接口是不是有点过分了?嗯,您已经确定了另一种可能的实现——test double。

(测试只是代码的一个消费者。因此,如果测试认为为角色指定特定实现的能力是有用的,那么这种感觉很可能与其他消费者共享。

现在,如果SUT确实依赖于OriginalThing(而不仅仅是任何可以DoesThing的旧对象),那么不应该允许用测试双精度对象替换它。在该场景中,测试应该验证SUT是否正确处理由OriginalThing填充的列表,而不是验证OriginalThing是否被调用。

修改代码使其可测试并不是一件坏事。事实上,这就是测试驱动开发背后的推动力。