如何模拟测试方法调用的实例方法,以便只测试一件事?

时间:2013-10-23 10:23:42

标签: c# unit-testing mocking rhino-mocks

想象一下,我有以下课程:

class ToTest : IToTest
{
    public string MethodA()
    {
        return "Test";
    }

    public int MethodB()
    {
        returns a random number;
    }

    public string MethodC()
    {
        return this.MethodA + " " + this.MethodB.ToString();
    }
}

我现在正在测试MethodC,所以我理解我应该模拟当前实例的MethodA和MethodB,所以我只测试MethodC的有效性吗?

我正在使用Rhino,我做了以下事情:

ToTest testedObject = new ToTest();

testedObject.Expect(t => t.MethodA).Returns("AString");
testedObject.Expect(t => t.MethodB).Returns(1324");

Assert.AreEqual("AString 1324", testedObject.MethodC());

但是我正确地说错误地说测试对象不是模拟。

方法是否正确?我该怎么办?

4 个答案:

答案 0 :(得分:4)

不,请不要写这样的测试用例。单元测试不是“只测试一件事”,而是关于确保每个测试都是一个单元,即它对任何其他测试都没有影响。

您所有的测试应该感兴趣的是您班级的公共API。不要测试内部或私有方法,它们是你班级内部工作的一部分,不要试图模仿你班级的部分来测试其他部分。您对MethodC 的测试必须间接测试MethodAMethodB,否则您的测试毫无意义。

对于有兴趣就如何编写好单元测试的优秀演讲感兴趣的人,我建议留出一小时并观看2013年NDC的'Ian Cooper: TDD, where did it all go wrong' video

答案 1 :(得分:3)

根据您让我们想象的例子,您问题的简单答案是您的方法不正确。通过遇到您遇到的麻烦,代码试图告诉您,您在代码中有一些混合的问题,这些问题耦合得太紧密,并且您没有使用有凝聚力的代码,而是使用粘性代码。 (Glenn Vanderburg在http://www.vanderburg.org/Blog/Software/Development/cohesion.rdoc发现了一个关于凝聚力主题的博客文章

如果您觉得需要模拟MethodAMethodB来测试MethodC,这是一个信号,表明其中一些事情,甚至可能全部都缺失了代码。

困难告诉你的第一件事是MethodC可能在代码中的错误位置。 MethodC很可能属于另一个以MethodAMethodB作为依赖关系的对象,通过构造函数注入或参数注入。

第二个突出的问题是,您在测试MethodC时遇到了问题,因为MethodB依赖于什么会导致我认为是全局/单身行为。您已将MethodB定义为returns a random number,而不是依赖于填充NumberGenerator角色的对象。通过显式调用对此角色的依赖(接口),它将允许您根据用途传递角色的不同实现。根据生产代码或测试代码的不同,可以使用并轻松交换的三个明显的实现是:

  • a RandomNumberGenerator:每次调用时都会返回一个随机数,
  • a SequentialNumberGenerator:返回序列中的下一个数字(可以生成多种类型的序列),例如每次增加一个设定的数字,或某种类型的数学序列,例如, Fibbonacci或Collat​​z
  • a ContsistantNumberGenerator:每次调用时返回相同的数字,非常在测试中有用。

你的例子可能告诉你的第三件事是对前一个问题的改进,并指出你不仅依赖于你无法控制的可变状态,而且依赖于非确定性的可变状态,你会即使向对象发送大量其他消息以试图进入该状态,也无法强制进入给定状态。我知道您没有可以发送的消息,这些消息将设置环境以在下次调用时返回所需的随机数以获取它;即使有这样的方式,所需的设置量也会模糊你实际想要测试的内容。

我希望其中一条建议可以帮助您解决问题。

答案 2 :(得分:1)

由于两个原因(假设您正在使用Rhino.Mocks),这将无效:

  • 您无法模拟非虚拟方法。
  • 您只能测试整个实例(或静态成员)。这意味着,当您想要在没有其他方法的情况下单独测试类中的方法时,您需要一些解决方法,即提供工厂或将MethodC移动到与MethodA和MethodB不同的类中。

答案 3 :(得分:1)

您的示例的工作代码将是这样的:

public class ToTest
{
    private Random random = new Random();

    public virtual string MethodA()
    {
        return "Test";
    }

    public virtual int MethodB()
    {
        return random.Next();
    }

    public virtual string MethodC()
    {
        return MethodA() + " " + MethodB();
    }
}

[TestFixture]
public class tests
{
    [Test]
    public void Test_MethodC()
    {
        var mocks = new MockRepository();
        ToTest testedObject = mocks.CreateMock<ToTest>();

        testedObject.Expect(t => t.MethodA()).Return("AString");
        testedObject.Expect(t => t.MethodB()).Return(1324);

        Assert.AreEqual("AString 1324", testedObject.MethodC());
    }
}

<强> BUT!

这不会通过测试。原因是 Rhino.Mocks 和大多数其他模拟框架一样,不包括MS Moles / Shims,TypeMock和其他使用profiler API的人,不能强制执行从内部查看模拟方法的类型。这是因为模拟框架围绕原始框架创建代理类型,并将模拟逻辑注入代理,而不是原始类型本身。

因此,正如建议的那样,您应该将 MethodA MethodB 提取到单独的依赖项中,以防您在 MethodC 中进行测试。

另一种完全可行的方法是从 ToTest 类派生,覆盖 MethodA MethodB ,并测试派生的实例类。