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

时间:2014-05-06 13:16:25

标签: c# unit-testing

如果我有课

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/

找到正在运行的示例

提前感谢您的阅读!

1 个答案:

答案 0 :(得分:3)

我建议阅读Growing Object-Oriented Software, Guided by Tests;事实证明,这是我们很多人遇到的问题。 您的测试试图告诉您某些事情,作为您代码的消费者。

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

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

  1. 如果OriginalThing实现了一个接口,情况会更容易;
  2. 如果您没有测试SUT对OriginalThing
  3. 的依赖性,事情会更容易

    您的SUT显然依赖于OriginalThing。可以使用Role Interface来描述此依赖关系吗? SUT真的只需要一些能够执行DoesThing的外部对象吗?如果是,那么您已经确定了seam in your code

    如果OriginalThing是您对此类角色的唯一实施,该怎么办?为某些只有一个实现的东西定义一个接口是不是太过分了?好吧,您已经确定了另一种可能的实现 - 测试双重。

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

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

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