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

本文关键字:测试 一件 实例方法 模拟 何模拟 测试方法 调用 | 更新日期: 2023-09-27 18:36:35

>想象一下我有以下类:

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

,所以我明白我应该模拟当前实例的方法 A 和 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());

但是我正确地收到一个错误,说测试对象不是模拟。

方法正确吗?我应该如何处理?

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

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

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

对于任何对如何编写好的单元测试的精彩演讲感兴趣的人,我建议留出一个小时,观看NDC 2013的"Ian Cooper:TDD,哪里出了问题"视频。

根据你让我们想象的例子,你的问题的简单答案是你的方法不正确。 通过遇到您遇到的麻烦,代码试图告诉您,您在代码中有一些混合问题,这些关注点耦合得太紧密,并且您没有内聚代码,而是粘性代码。(格伦·范德堡(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 或 Collatz
  • a ContsistantNumberGenerator : 每次调用时都返回相同的数字,这在测试中非常有用

您的示例可能告诉您的第三件事是对上一个问题的改进,指出您不仅依赖于您无法控制的可变状态,还依赖于非确定性可变状态,即使通过向对象发送许多其他消息以尝试进入该状态,您也无法强制进入给定状态。 我知道您可以发送的任何消息都可以将环境设置为在下次调用以获取它时返回所需的随机数;即使有这样的方法,所需的设置量也会掩盖您实际尝试测试的内容。

我希望这些建议之一可以帮助您解决问题。

这行不通,原因有两个(假设你使用的是Rhino.Mocks):

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

示例的工作代码如下所示:

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());
    }
}

但!

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

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

另一种完全可行的方法是从 ToTest 类派生,重写 MethodAMethodB,并测试派生类的实例。