Как написать тест Mockist рекурсивного метода - PullRequest
8 голосов
/ 11 февраля 2010

Если у меня есть метод, который вызывает себя при определенных условиях, можно ли написать тест для проверки поведения? Мне бы очень хотелось увидеть пример, мне наплевать на фальшивые рамки или язык. Я использую RhinoMocks в C #, поэтому мне любопытно, является ли это отсутствующей функцией каркаса, или я неправильно понимаю что-то фундаментальное, или это просто невозможно.

Ответы [ 5 ]

6 голосов
/ 03 марта 2010

метод, который вызывает себя при определенных условиях, возможно ли написать тест для проверки поведения?

Да. Однако, если вам нужно протестировать рекурсию, вам лучше разделить точку входа в рекурсию и шаг рекурсии для целей тестирования.

В любом случае, вот пример, как проверить это, если вы не можете этого сделать. Вам не нужно издеваться:

// 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 голосов
/ 03 марта 2010

Вы неправильно понимаете цель фиктивных объектов. Насмешки (в смысле Мокиста) используются для проверки поведенческих взаимодействий с зависимостями тестируемой системы.

Так, например, у вас может быть что-то вроде этого:

interface IMailOrder
{
   void OrderExplosives();
}

class Coyote
{
   public Coyote(IMailOrder mailOrder) {}

   public void CatchDinner() {}
}

Койот зависит от IMailOrder. В рабочем коде экземпляру Coyote будет передан экземпляр Acme, который реализует IMailOrder. (Это может быть сделано с помощью ручного внедрения зависимостей или через структуру DI.)

Вы хотите протестировать метод CatchDinner и убедиться, что он вызывает OrderExplosives. Для этого вы:

  1. Создание фиктивного объекта, реализующего IMailOrder, и создание экземпляра Coyote (тестируемой системы) путем передачи фиктивного объекта его конструктору. (Arrange)
  2. Call CatchDinner. (Закон)
  3. Попросите фиктивный объект проверить, что данное ожидание (вызванное OrderExplosives) было удовлетворено. (Assert)

Когда вы настраиваете ожидания для фиктивного объекта, это может зависеть от вашей фальшивой (изолированной) среды.

Если тестируемый класс или метод не имеет внешних зависимостей, вам не нужно (или не нужно) использовать фиктивные объекты для этого набора тестов. Не имеет значения, является ли метод рекурсивным или нет.

Обычно вы хотите проверить граничные условия, поэтому вы можете проверить вызов, который не должен быть рекурсивным, вызов с одним рекурсивным вызовом и глубоко рекурсивный вызов. (у miaubiz есть хорошая точка зрения о том, что рекурсия - это деталь реализации.)

РЕДАКТИРОВАТЬ: Под "вызовом" в последнем абзаце я имел в виду вызов с параметрами или состоянием объекта, который вызовет заданную глубину рекурсии. Я также рекомендовал бы прочитать Искусство модульного тестирования .

РЕДАКТИРОВАТЬ 2: Пример тестового кода с использованием Moq :

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

wily.CatchDinner();

mockMailOrder.Verify(x => x.OrderExplosives());
4 голосов
/ 28 февраля 2010

Если вы хотите сделать что-то вроде получения имени файла по полному пути, например:

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
1 голос
/ 11 февраля 2010

Нет ничего, что могло бы контролировать глубину стека / количество (рекурсивных) вызовов функций в какой-либо моделируемой среде, о которой я знаю. Тем не менее, модульное тестирование на то, что правильные смоделированные предварительные условия обеспечивают правильные выходные данные, должно быть таким же, как и моделирование нерекурсивной функции.

Бесконечная рекурсия, приводящая к переполнению стека, которую вам придется отлаживать отдельно, но модульные тесты и макеты никогда не избавлялись от этой необходимости.

0 голосов
/ 03 марта 2010

Вот мой «крестьянский» подход (в 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
...