【问题标题】:How to write a Mockist test of a recursive method如何编写递归方法的 Mockist 测试
【发布时间】:2011-01-15 13:16:19
【问题描述】:

如果我有一个在特定条件下调用自身的方法,是否可以编写一个测试来验证该行为?我很想看一个例子,我不关心模拟框架或语言。我在 C# 中使用 RhinoMocks,所以我很好奇它是否是框架缺少的功能,或者我误解了一些基本的东西,或者这只是一个不可能。

【问题讨论】:

  • 我不清楚。究竟要测试什么?该方法在“特定条件下”调用自身(“调用堆栈”将遵循“在特定条件下”的特定路径)或其他什么?

标签: unit-testing mocking rhino-mocks moq


【解决方案1】:

在我知道的任何模拟框架中,没有任何东西可以监控堆栈深度/(递归)函数调用的数量。但是,正确模拟前置条件提供正确输出的单元测试应该与模拟非递归函数相同。

导致堆栈溢出的无限递归您必须单独调试,但单元测试和模拟从一开始就没有摆脱这种需求。

【讨论】:

    【解决方案2】:

    假设你想做一些事情,比如从完整路径中获取文件名,例如:

    c:/windows/awesome/lol.cs -> lol.cs
    c:/windows/awesome/yeah/lol.cs -> lol.cs
    lol.cs -> lol.cs
    

    你有:

    public getFilename(String original) {
      var stripped = original;
      while(hasSlashes(stripped)) {
        stripped = stripped.substringAfterFirstSlash(); 
      }
      return stripped;
    }
    

    你想写:

    public getFilename(String original) {
      if(hasSlashes(original)) {
        return getFilename(original.substringAfterFirstSlash()); 
      }
      return original;
    }
    

    这里的递归是一个实现细节,不应进行测试。您真的希望能够在两种实现之间切换并验证它们是否产生相同的结果:对于上面的三个示例,它们都产生 lol.cs。

    话虽如此,因为您是按名称递归,而不是说 thisMethod.again() 等,在 Ruby 中,您可以将原始方法别名为新名称,用旧名称重新定义方法,调用新名称并检查您是否以新定义的方法结束。

    def blah
      puts "in blah"
      blah
    end
    
    alias blah2 blah
    
    def blah
      puts "new blah"
    end
    
    blah2
    

    【讨论】:

    • 你是说在这种情况下,验证方法状态的单元测试就足够了?
    • 对不起,我不完全理解你的问题。在我的文件路径示例中,验证方法输出的单元测试就足够了,甚至比验证递归的单元测试更好。不过,我不知道你的具体情况,所以可能会有所不同。
    • @jayrdub - 一般来说,状态验证正是您希望单元测试执行的操作。检查方法的返回值和/或被测对象的公共属性。其他一切都是实现细节,在重构过程中可能会发生变化。
    【解决方案3】:

    在一定条件下调用自己的方法,是否可以编写测试来验证其行为?

    是的。但是,如果您需要测试递归,最好将递归的入口点和递归步骤分开以进行测试。

    无论如何,如果你不能这样做,这里是如何测试它的示例。你真的不需要任何嘲笑:

    // Class under test
    public class Factorial
    {
        public virtual int Calculate(int number)
        {
            if (number < 2)
                return 1
            return Calculate(number-1) * number;
        }
    }
    
    // The helper class to test the recursion
    public class FactorialTester : Factorial
    {
        public int NumberOfCalls { get; set; }
    
        public override int Calculate(int number)
        {
            NumberOfCalls++;
            return base.Calculate(number)
        }
    }    
    
    // Testing
    [Test]
    public void IsCalledAtLeastOnce()
    {
        var tester = new FactorialTester();
        tester.Calculate(1);
        Assert.GreaterOrEqual(1, tester.NumberOfCalls  );
    }
    [Test]
    public void IsCalled3TimesForNumber3()
    {
        var tester = new FactorialTester();
        tester.Calculate(3);
        Assert.AreEqual(3, tester.NumberOfCalls  );
    }
    

    【讨论】:

      【解决方案4】:

      您误解了模拟对象的目的。 Mocks(在 Mockist 意义上)用于测试与被测系统的依赖关系的行为交互。

      所以,例如,你可能有这样的事情:

      interface IMailOrder
      {
         void OrderExplosives();
      }
      
      class Coyote
      {
         public Coyote(IMailOrder mailOrder) {}
      
         public void CatchDinner() {}
      }
      

      Coyote 依赖于 IMailOrder。在生产代码中,将向 Coyote 的实例传递一个 Acme 的实例,该实例实现了 IMailOrder。 (这可以通过手动依赖注入或通过 DI 框架来完成。)

      您想测试方法 CatchDinner 并验证它是否调用了 OrderExplosives。为此,您:

      1. 创建一个实现 IMailOrder 的模拟对象,并通过将模拟对象传递给其构造函数来创建 Coyote(被测系统)的实例。 (排列)
      2. 致电 CatchDinner。 (法案)
      3. 要求模拟对象验证是否满足给定的期望(称为 OrderExplosives)。 (断言)

      当您设置对模拟对象的期望时,可能取决于您的模拟(隔离)框架。

      如果您正在测试的类或方法没有外部依赖项,则您不需要(或不想)为该组测试使用模拟对象。方法是否递归无关紧要。

      您通常希望测试边界条件,因此您可能会测试不应递归的调用、具有单个递归调用的调用以及深度递归调用。 (不过,miaubiz 有一个很好的观点,即递归是一个实现细节。)

      编辑:最后一段中的“调用”是指带有参数或对象状态的调用,它会触发给定的递归深度。我还建议阅读The Art of Unit Testing

      编辑 2: 使用 Moq 的示例测试代码:

      var mockMailOrder = new Mock<IMailOrder>();
      var wily = new Coyote(mockMailOrder.Object);
      
      wily.CatchDinner();
      
      mockMailOrder.Verify(x => x.OrderExplosives());
      

      【讨论】:

      • "如果您正在测试的类或方法没有外部依赖项,则您不需要(或不想)为该组测试使用模拟对象。方法是否无关紧要是否递归。”这是我需要提醒的部分,谢谢。我最喜欢你的回答,但它在我之前自动选择了。
      【解决方案5】:

      这是我的“农民”方法(在 Python 中,经过测试,请参阅 cmets 了解基本原理)

      请注意,这里的实现细节“暴露”是没有问题的,因为您正在测试的是恰好被“顶级”代码使用的底层架构。因此,对其进行测试是合法且行为良好的(我也希望这是您的想法)。

      代码(主要思想是从单个但“不可测试”的递归函数转换为等效的一对递归相关(因此可测试)函数):

      def factorial(n):
          """Everyone knows this functions contract:)
          Internally designed to use 'factorial_impl' (hence recursion)."""
          return factorial_impl(n, factorial_impl)
      
      def factorial_impl(n, fct=factorial):
          """This function's contract is
          to return 'n*fct(n-1)' for n > 1, or '1' otherwise.
      
          'fct' must be a function both taking and returning 'int'"""
          return n*fct(n - 1) if n > 1 else 1
      

      测试:

      import unittest
      
      class TestFactorial(unittest.TestCase):
      
          def test_impl(self):
              """Test the 'factorial_impl' function,
              'wiring' it to a specially constructed 'fct'"""
      
              def fct(n):
                  """To be 'injected'
                  as a 'factorial_impl''s 'fct' parameter"""
                  # Use a simple number, which will 'show' itself
                  # in the 'factorial_impl' return value.
                  return 100
      
              # Here we must get '1'.
              self.assertEqual(factorial_impl(1, fct), 1)
              # Here we must get 'n*100', note the ease of testing:)
              self.assertEqual(factorial_impl(2, fct), 2*100)
              self.assertEqual(factorial_impl(3, fct), 3*100)
      
          def test(self):
              """Test the 'factorial' function"""
              self.assertEqual(factorial(1), 1)
              self.assertEqual(factorial(2), 2)
              self.assertEqual(factorial(3), 6)
      

      输出:

      Finding files...
      ['...py'] ... done
      Importing test modules ... done.
      
      Test the 'factorial' function ... ok
      Test the 'factorial_impl' function, ... ok
      
      ----------------------------------------------------------------------
      Ran 2 tests in 0.000s
      
      OK
      

      【讨论】:

      • 还有递归测试?
      • @britodfbr 被认为没有必要,因为(自我)依赖项已在顶层显式传递/注入,我认为。已经9年了:)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-19
      • 1970-01-01
      • 2014-05-21
      • 2019-08-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多