如何编写递归方法的Mockist测试

时间:2010-02-11 02:05:05

标签: unit-testing mocking rhino-mocks moq

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

5 个答案:

答案 0 :(得分:6)

  

在某种条件下调用自身的方法,是否可以编写测试来验证行为?

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

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

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

答案 1 :(得分:4)

假设您想要从完整路径中获取文件名,例如:

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。

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

def blah
  puts "in blah"
  blah
end

alias blah2 blah

def blah
  puts "new blah"
end

blah2

答案 2 :(得分:4)

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

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

interface IMailOrder
{
   void OrderExplosives();
}

class Coyote
{
   public Coyote(IMailOrder mailOrder) {}

   public void CatchDinner() {}
}

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

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

  1. 创建一个实现IMailOrder的模拟对象,并通过将模拟对象传递给其构造函数来创建Coyote实例(被测系统)。 (排列)
  2. 致电CatchDinner。 (动作)
  3. 要求模拟对象验证是否满足给定的期望(OrderExplosives已调用)。 (断言)
  4. 当您设置模拟对象的期望可能取决于您的模拟(隔离)框架。

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

    您通常希望测试边界条件,因此您可以测试不应该递归的调用,使用单个递归调用的调用以及深度递归调用。 (尽管如此,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());
    

答案 3 :(得分:1)

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

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

答案 4 :(得分:0)

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

请注意,实现细节“曝光”在这里是不可能的,因为您正在测试的是“顶级”代码恰好使用的底层架构。因此,测试它是合法且表现良好的(我也希望,这是你的想法)。

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

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