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

发布于 2024-08-20 20:33:08 字数 137 浏览 4 评论 0原文

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

If I have a method that calls itself under a certain condition, is it possible to write a test to verify the behavior? I'd love to see an example, I don't care about the mock framework or language. I'm using RhinoMocks in C# so I'm curious if it is a missing feature of the framework, or if I'm misunderstanding something fundamental, or if it is just an impossibility.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

听你说爱我 2024-08-27 20:33:08

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

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

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

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

a method that calls itself under a certain condition, is it possible to write a test to verify the behavior?

Yes. However, if you need to test recursion you better separate the entry point into the recursion and the recursion step for testing purposes.

Anyway, here is the example how to test it if you cannot do that. You don't really need any mocking:

// 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  );
}
临风闻羌笛 2024-08-27 20:33:08

假设您想要执行类似从完整路径获取文件名的操作,例如:

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

Assuming you want to do something like get the filename from a complete path, for example:

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

and you have:

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

and you want to write:

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

Recursion here is an implementation detail and should not be tested for. You really want to be able to switch between the two implementations and verify that they produce the same result: both produce lol.cs for the three examples above.

That being said, because you are recursing by name, rather than saying thisMethod.again() etc., in Ruby you can alias the original method to a new name, redefine the method with the old name, invoke the new name and check whether you end up in the newly defined method.

def blah
  puts "in blah"
  blah
end

alias blah2 blah

def blah
  puts "new blah"
end

blah2
寄离 2024-08-27 20:33:08

您误解了模拟对象的目的。模拟(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)。 (断言)

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

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

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

编辑:上一段中的“调用”是指带有参数或对象状态的调用,该调用将触发给定的递归深度。我还建议阅读单元测试的艺术

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

var mockMailOrder = new Mock<IMailOrder>();
var wily = new Coyote(mockMailOrder.Object);

wily.CatchDinner();

mockMailOrder.Verify(x => x.OrderExplosives());

You're misunderstanding the purpose of mock objects. Mocks (in the Mockist sense) are used to test behavioral interactions with dependencies of the system under test.

So, for instance, you might have something like this:

interface IMailOrder
{
   void OrderExplosives();
}

class Coyote
{
   public Coyote(IMailOrder mailOrder) {}

   public void CatchDinner() {}
}

Coyote depends on IMailOrder. In production code, an instance of Coyote would be passed an instance of Acme, which implements IMailOrder. (This might be done through manual Dependency Injection or via a DI framework.)

You want to test method CatchDinner and verify that it calls OrderExplosives. To do so, you:

  1. Create a mock object that implements IMailOrder and create an instance of Coyote (the system under test) by passing the mock object to its constructor. (Arrange)
  2. Call CatchDinner. (Act)
  3. Ask the mock object to verify that a given expectation (OrderExplosives called) was met. (Assert)

When you setup the expectations on the mock object may depend on your mocking (isolation) framework.

If the class or method you're testing has no external dependencies, you don't need (or want) to use mock objects for that set of tests. It doesn't matter if the method is recursive or not.

You generally want to test boundary conditions, so you might test a call that should not be recursive, a call with a single recursive call, and a deeply-recursive call. (miaubiz has a good point about recursion being an implementation detail, though.)

EDIT: By "call" in the last paragraph I meant a call with parameters or object state that would trigger a given recursion depth. I'd also recommend reading The Art of Unit Testing.

EDIT 2: Example test code using Moq:

var mockMailOrder = new Mock<IMailOrder>();
var wily = new Coyote(mockMailOrder.Object);

wily.CatchDinner();

mockMailOrder.Verify(x => x.OrderExplosives());
吾家有女初长成 2024-08-27 20:33:08

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

无限递归会导致堆栈溢出,您必须单独调试,但单元测试和模拟从未摆脱这种需求。

There isn't anything to monitor stack depth/number of (recursive) function calls in any mocking framework I'm aware of. However, unit testing that the proper mocked pre-conditions provide the correct outputs should be the same as mocking a non-recursive function.

Infinite recursion that leads to a stack overflow you'll have to debug separately, but unit tests and mocks have never gotten rid of that need in the first place.

£烟消云散 2024-08-27 20:33:08

这是我的“农民”方法(在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

Here's my 'peasant' approach (in Python, tested, see the comments for the rationale)

Note that implementation detail "exposure" is out of question here, since what you are testing is the underlying architecture which happens to be utilized by the "top-level" code. So, testing it is legitimate and well-behaved (I also hope, it's what you have in mind).

The code (the main idea is to go from a single but "untestable" recursive function to an equivalent pair of recursively dependent (and thus testable) functions):

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

The test:

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)

The output:

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文