代码之家  ›  专栏  ›  技术社区  ›  Ignacio Soler Garcia

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

  •  1
  • Ignacio Soler Garcia  · 技术社区  · 11 年前

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

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

    但我得到了一个错误,正确地说testedObject不是Mock。

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

    4 回复  |  直到 11 年前
        1
  •  5
  •   David Arno    11 年前

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

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

    对于任何有兴趣就如何编写好的单元测试进行精彩演讲的人,我建议留出一个小时的时间观看 'Ian Cooper: TDD, where did it all go wrong' video 来自NDC 2013。

        2
  •  3
  •   Proctor    11 年前

    根据你让我们想象的例子,你的问题的简单答案是你的方法不正确。通过遇到您正在经历的问题,代码试图告诉您,您在代码中有一些混合的问题,这些问题耦合得太紧密了,而且您使用的不是内聚代码,而是粘合代码。(Glenn Vanderburg在 http://www.vanderburg.org/Blog/Software/Development/cohesion.rdoc )

    如果你觉得有必要模仿 MethodA MethodB 测试 MethodC 这是一个信号,表明代码中缺少了一些东西中的一个,甚至可能是所有的。

    困难告诉你的第一件事是 方法C 可能在代码中的错误位置。 方法C 很可能属于另一个物体 方法A 方法B 作为依赖项,可以通过构造函数注入,也可以通过参数注入。

    第二件突出的事情是,您在测试时遇到了困难 方法C 因为 方法B 依赖于什么会让我认为是全局/单例行为。您已定义 方法B returns a random number ,而不是对填充角色的对象进行依赖 NumberGenerator 。通过显式调用对此角色(接口)的依赖关系,它将允许您根据使用情况传入角色的不同实现。根据是生产代码还是测试代码,可以使用并轻松交换的三种明显实现是:

    • RandomNumberGenerator :每次调用时返回一个随机数,
    • SequentialNumberGenerator :返回序列中的下一个数字(可以生成多种类型的序列),例如每次增加一个组号,或者某种数学序列,例如Fibbonacci或Collatz
    • ContsistantNumberGenerator :每次调用时返回相同的数字,即 非常 在测试中很有用。

    您的示例可能会告诉您的第三件事是对前一个问题的改进,指出您不仅依赖于无法控制的可变状态,而且依赖于不确定的可变状态。即使向对象发送许多其他消息以尝试进入该状态,您也无法将其强制进入给定状态。据我所知,您无法发送任何消息来设置环境以返回所需的 随机数 在下一次呼叫时获取;即使有这样的方法,所需的设置量也会使您实际尝试测试的内容模糊不清。

    我希望其中一条建议能帮助你解决你的问题。

        3
  •  1
  •   PMF    11 年前

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

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

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

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

    但是!

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

    所以,正如建议的那样,你应该提取 方法A 方法B 转换为单独的依赖项,以防您有需要测试的内容 方法C .

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