Как записать тест Mockist рекурсивного метода

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

8
задан JeremyWeir 11 February 2010 в 02:05
поделиться

5 ответов

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

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
4
ответ дан 5 December 2019 в 11:24
поделиться

Если вы действительно хотите охватить все базы, попробуйте сделать:

setcookie (session_id(), "", time() - 3600);
session_destroy();
session_write_close();

Это должно предотвратить дальнейший доступ к данным сеанса для остальной части выполнения PHP. Браузер может по-прежнему отображать файл cookie, однако $ _ SESSION super будет пустым

-121--2326175-

Я не совсем уверен, какова ваша цель. Чтобы изменить размер шрифта или размер остальной части картины? Существует несколько возможностей для достижения любой из следующих возможностей:

Мой пример коробки с 1 см x 1 см большой коробки:

\begin{tikzpicture}
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}

Масштабировать графические элементы, но НЕ текст:

\begin{tikzpicture}[scale=0.5]
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}

\begin{tikzpicture}[scale=0.5]
    \draw (0,0) rectangle (1cm,1cm) -- +(-1,-1);
    \node(text) at (0.5cm,0.5cm) {Text};
\end{tikzpicture}

Масштабировать только координаты (то есть, если единица измерения не указана, используется умножение указанного вектора x, y и z):

\begin{tikzpicture}[x=5mm,y=5mm]
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}

Явные длины при этом не изменяются:

\begin{tikzpicture}[x=5mm,y=5mm]
    \draw (0,0) rectangle (1cm,1cm) -- +(-1cm,-1cm);
    \node(text) at (0.5cm,0.5cm) {Text};
\end{tikzpicture}

Масштабировать все, даже текст (согласно pgfmanual, это не рекомендуется):

\begin{tikzpicture}[transform canvas={scale=0.5}]
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}

Тот же эффект с помощью команд LaTeX:

\scalebox{0.5}{
\begin{tikzpicture}
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}
}

Измените размер шрифта локально, но размер графических элементов не изменяется:

\begin{tikzpicture}[font=\scriptsize]
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}

Измените размер шрифта во всех последующих средах tikzpicture (\tikzset также можно использовать для установки указанных выше параметров):

\tikzset{font=\scriptsize}
\begin{tikzpicture}
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}

\tikzset остается локальным в группах:

\begin{minipage}{\linewidth}
\tikzset{font=\scriptsize}
\begin{tikzpicture}
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}
\end{minipage}

{
\tikzset{font=\scriptsize}
\begin{tikzpicture}
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}
}

\begin{tikzpicture}
    \draw (0,0) rectangle (1,1) -- +(-1,-1);
    \node(text) at (0.5,0.5) {Text};
\end{tikzpicture}

Я не знаю, можно ли редактировать файлы graph? .tex , или они созданы? Возможно, можно передать параметры в программу, создающую среду tikzpicture . В противном случае просто задайте требуемый параметр в нужной области с помощью \tikzset .

Надеюсь, это помогает.

ПРАВКА : Если опции (например, font = ) определены локально в graph? .tex , то они получают приоритет над опциями, определенными снаружи. Если это так, вы просто не можете перезаписать их снаружи. Вам придется отредактировать файлы.

EDIT : Этот «минимальный» пример работает для меня (показывая действительно большой «Mackenzie Net Sales», который превышает ширину субфигуры)

\documentclass{article}

\usepackage{fix-cm}
\usepackage{subfig}
\usepackage{tikz}

\begin{document}
\tikzset{every picture/.append style={font=\fontsize{100}{120}\selectfont}}

\begin{figure}[h]
\centering
\subfloat[Graph 1]{
    \begin{minipage}[h]{0.7\linewidth}
        \centering\beginpgfgraphicnamed{graph1}
%        \input{graph1.tex}
        \endpgfgraphicnamed
        \label{fig:graph1}
    \end{minipage}}
\hspace{5pt}
\subfloat[Graph 2]{
    \begin{minipage}[h]{0.5\linewidth}
        \centering 
        \resizebox{\textwidth}{!}{

            %\beginpgfgraphicnamed{graph2}  
\begin{tikzpicture}
\begin{scope}
\path[clip] (  0.00,  0.00) rectangle (79.497,61.429);
\definecolor[named]{drawColor}{rgb}{0.13,0.76,0.43}
\definecolor[named]{fillColor}{rgb}{0.31,0.94,0.66}
\end{scope}
\begin{scope}
\path[clip] (  0.00,  0.00) rectangle (79.497,61.429);
\definecolor[named]{drawColor}{rgb}{0.13,0.76,0.43}
\definecolor[named]{fillColor}{rgb}{0.31,0.94,0.66}
\end{scope}
\begin{scope}
\path[clip] (  0.00,  0.00) rectangle (79.497,61.429);
\definecolor[named]{drawColor}{rgb}{0.13,0.76,0.43}
\definecolor[named]{fillColor}{rgb}{0.31,0.94,0.66}
\definecolor[named]{fillColor}{rgb}{1.00,1.00,1.00}
\draw[fill=fillColor,draw opacity=0.00,] (  0.00,  0.00) rectangle (79.497,61.429);
\end{scope}
\begin{scope}
\path[clip] (  0.00,  0.00) rectangle (79.497,61.429);
\definecolor[named]{drawColor}{rgb}{0.13,0.76,0.43}
\definecolor[named]{fillColor}{rgb}{0.31,0.94,0.66}
\definecolor[named]{drawColor}{rgb}{0.00,0.00,0.00}
\node[rotate= 90.00,color=drawColor,anchor=base,inner sep=0pt, outer sep=0pt, scale=  1.00] at ( 1.592,31.059) {Mackenzie Net Sales};
\end{scope}
\begin{scope}
\path[clip] (  0.00,  0.00) rectangle (79.497,61.429);
\definecolor[named]{drawColor}{rgb}{0.13,0.76,0.43}
\definecolor[named]{fillColor}{rgb}{0.31,0.94,0.66}
\end{scope}
\end{tikzpicture}
}
        %\endpgfgraphicnamed

        \label{fig:graph2}
    \end{minipage}}
\subfloat[Graph 3]{
    \begin{minipage}[h]{0.5\linewidth}
        \centering\beginpgfgraphicnamed{graph3}
%        \input{graph3.tex}
        \endpgfgraphicnamed
        \label{fig:graph3}
    \end{minipage}}
\caption{Three Graphs}
\end{figure}

\end{document}

Пожалуйста, проверьте, работает ли это и для вас. Если да, то попытайтесь изменить его, пока он больше не заработает. Что такое перемена?

-121--3536952-

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

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

1
ответ дан 5 December 2019 в 11:24
поделиться

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

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

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

// 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  );
}
6
ответ дан 5 December 2019 в 11:24
поделиться

Вот мой «крестьянский» подход (протестирован на 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
0
ответ дан 5 December 2019 в 11:24
поделиться

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

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

interface IMailOrder
{
   void OrderExplosives();
}

class Coyote
{
   public Coyote(IMailOrder mailOrder) {}

   public void CatchDinner() {}
}

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

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

  1. Создаете объект-макет, реализующий IMailOrder, и создаете экземпляр Coyote (тестируемой системы), передавая объект-макет в его конструктор. (Организовать)
  2. Вызовите CatchDinner. (Act)
  3. Попросите объект-макет проверить, что заданное ожидание (OrderExplosives called) было выполнено. (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());
4
ответ дан 5 December 2019 в 11:24
поделиться
Другие вопросы по тегам:

Похожие вопросы: