2010-02-11 3 views
8

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

+0

Это мне не ясно. Что именно пытаются проверить? То, что метод называет себя «при определенных условиях» (что «стек вызовов» будет следовать определенному пути «при определенных условиях») или что-то еще? – Ando

ответ

3

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

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 
+0

Вы говорите, что в этом случае единичный тест, который проверяет состояние метода, достаточно хорош? – JeremyWeir

+0

Извините, я не совсем понимаю ваш вопрос. В примере с файловым каналом достаточно выполнить единичный тест, который проверяет вывод метода, или даже лучше, чем тот, который проверяет рекурсию. Однако я не знаю вашей конкретной ситуации, так что это может быть иначе. – miaubiz

+0

@jayrdub - Проверка состояния - это именно то, что вы хотите, чтобы ваши модульные тесты выполнялись. Проверьте возвращаемое значение метода и/или общедоступные свойства тестируемого объекта. Все остальное является детальностью реализации и может изменяться во время рефакторинга. – TrueWill

1

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

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

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 ); 
} 
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. Call CatchDinner. (Закон)
  3. Задайте объект-макет, чтобы убедиться, что данное ожидание (OrderExplosives) было выполнено. (Assert)

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

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

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

EDIT: К «вызову» в последнем абзаце, я подразумевал, вызов с параметрами или состояние объекта, который будет инициировать заданную глубину рекурсии. Я также рекомендую прочитать The Art of Unit Testing.

EDIT 2: Пример тестового кода с использованием Moq:

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

wily.CatchDinner(); 

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

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

+0

@jayrdub - Спасибо! :) – TrueWill

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 
Смежные вопросы