Написание динамических модульных тестов (flexunit) сбивает с толку. Как я могу сделать это более модульным? - PullRequest
3 голосов
/ 09 марта 2012

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

назад информация

Я пишу несколько юнит-тестов (ууу!). у меня есть 40 объектов, которые реализуют интерфейс. одна функция в этом интерфейсе принимает два параметра, один Rectangle и один массив Rectangle:

public function foobar(foo:Rectangle, bar:Array/*Rectangle*/):void;

Я хочу написать тесты для каждого из этих 40 объектов. чтобы убедиться, что я тестирую все возможности, мне нужно запустить тесты, где есть вариации foo и вариации bar (по длине и содержанию). так х число foo и от 1 до х число прямоугольника в foo.

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

если бы я выбрал 10 возможных объектов foo и 10 возможных объектов для массива bar, я бы написал тысячи! испытаний. я не хочу, чтобы вручную написать тысячи тестов.


вопросы

было бы слишком поздно для меня написать алгоритм, который бы брал возможные объекты и выполнял тесты на всех возможных конфигурациях, дающих результаты, а затем я возвращался и проверял, что все результаты верны? это просто неправильный способ проведения юнит-тестов?

Неправильно ли запускать алгоритм, который дает результаты, ТО вручную проверять вывод?

Моя другая мысль состояла бы в том, что я передаю алгоритму возможные объекты, и он выплевывает некоторый xml или json, отформатированный для тестового набора, затем я прохожу каждый тест, заполняя пропущенные значения утверждений, затем прокормить их?

Мой другой план - написать алгоритм, который принимает список foo Rectangle и список возможных Rectangle для использования в bar, и заставить этот алгоритм генерировать JSON в формате, который работает с моим тестовым набором (он включает в себя утверждения). поскольку алгоритм, создающий JSON, не будет знать утверждений, я бы написал их перед отправкой через тестовый комплект.

это обычная практика?


спасибо за любые отзывы:)

1 Ответ

2 голосов
/ 10 марта 2012

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

0. Большие ожидания?

Если честно, вероятно, нет ответа, который точно соответствует вашему вопросу - многие способы сделать что-то сделают работу правильно, и единственное фундаментальное правило, которое применяется к модульным тестам, - это то, что они должны надежно помочь вам доказать, что ваша система стабильна . Если они этого не делают, не стоит их писать. Но если это можно сделать, создав лист Excel с миллионом строк различных комбинаций входных и выходных значений и передав его в формате CSV в цикл for в модульном тесте ...

ОК, может быть, лучше . Но, в конце концов, все зависит от того, насколько тщательно вы хотите это сделать, и насколько вы готовы отклониться от того, что вы уже сделали, чтобы улучшить свои тесты.

1. Будьте готовы к некоторым умным замечаниям

Из того, что я прочитал между строк, вы не тратите много времени на размышления о тестируемости, потому что вы написали свой код перед написанием тестов. К сожалению, это на самом деле не лучший способ выполнения модульных тестов: каждая строка, которую вы добавляете в производственный код, всегда должна покрываться неудачным модульным тестом , прежде чем вы даже напишите его . Только так вы всегда можете быть уверены, что ваша система работает - и она всегда тестируема! Звучит скучно? Это не так, как только вы привыкнете к этому.

Я не буду слишком беспокоить вас основами, но если вы действительно серьезно относитесь к юнит-тестам, просто позвольте мне порекомендовать вам начать применять TDD ко всем будущим проектам: для начала, возможно, посмотрите эпизоды TDD на cleancoders.com - Дядя Боб гораздо лучше объясняет эти вещи, чем я, и ему интересно наблюдать (хотя его демонстрации на Java, но это не должно быть проблема - фундаментальные принципы TDD распространяются на все языки).

А пока я все же сделаю несколько умных замечаний на основе вашего вопроса. Подайте в суд;)

2. Умное замечание № 1: Как проверить?

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

Это ответит на ваш первый вопрос: У вас должен быть только один тест, чтобы доказать правильность для каждого тестируемого вами алгоритма. Для утверждений в рамках этого теста могут использоваться различные комбинации входных и выходных значений. .

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

Что немного сложнее, так это выбор того, на каком уровне абстракции вы начинаете писать свои тесты. Обычно нет необходимости писать тест для каждого метода в классе, особенно для частных. Это еще одна причина для применения TDD - это заставляет вас думать о том, что вы пытаетесь делать извне, то есть вы проверяете, что часть вашей системы должна делать , вместо того, чтобы тестировать каждую реализацию подробно. Когда вы тестируете перед написанием кода, легко добавить тест здесь и там, когда вы заметите, что ваша программа выросла и все усложнилось; всегда намного труднее сделать это «после факта».

3. Замечательное замечание № 2: Что тестировать?

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

  • убедитесь, что каждый тестируемый вами метод выполняет только одну вещь (!) И

  • все остальные вещи, необходимые в этом методе, должны быть либо переданы в качестве аргументов, либо предоставлены классу как переменные поля - позвольте мне прояснить это: не нужно создавать объекты внутри вашего метода, если только они не являются временными переменными или вернуть значения! Внешние зависимости, тогда вы должны заменить test doubles при тестировании метода.

4. Слабая попытка применить это к вашей проблеме

Почему я говорю вам все это? Мне кажется, что ваш подход больше похож на интеграционный тест : есть черный ящик для тестирования, и из него могут выйти миллионы вещей. Это нормально для некоторой части, но вы все равно должны попытаться сделать этот черный ящик как можно меньше.

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

Очевидное первое предположение: вы неоднократно применяете одни и те же вычисления ко всем элементам массива bar, основываясь на значениях координат прямоугольника foo. Это будет означать, что вы на самом деле делаете две вещи в своем методе: а) перебираете массив bar и б) применяете формулу:

public function foobar ( foo:Rectangle, bar:Array ) : void {
    for each ( var rect:Rectangle in bar) {
        // things done to rect based on foo
    }
}

Если это так, вы можете легко улучшить свою архитектуру. Первым шагом будет выделение формулы:

public function foobar ( foo:Rectangle, bar:Array ) : void {
    for each ( var rect:Rectangle in bar) {
        applyFooValuesToRect( foo, rect);
    }
}

public function applyFooValuesToRect ( foo : Rectangle, rect : Rectangle ) : void {
    // things done to rect based on foo
} 

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

Я также могу предположить, что на итерации могут быть разные варианты: одна реализация применяется foo ко всем bar, одна соответствует некоторым критериям и применяется только к положительным совпадениям, может быть, одна выполняет цепочку вычислений на foo основываясь на каждом из значений bar, можно использовать две формулы вместо одной и т. д. Если что-то из этого применимо к вашему проекту, вы можете значительно улучшить свой API и уменьшить сложность, используя шаблон стратегии . Для каждого из 40 вариантов сделайте фактическую формулу отдельным классом, который реализует общий интерфейс Formula:

public interface Formula {
    function applyFooToBar (foo:Rectangle, bar:Rectangle) : Rectangle;
}

public class FormulaOneImpl implements Formula {

    public function applyFooToBar (foo:Rectangle, bar:Rectangle) : Rectangle {
        // do things to bar
        return bar;
    }
}

public class FormulaTwoImpl implements Formula ... // etc.

Теперь вы можете проверить каждую формулу отдельно и применить утверждения к возвращенному значению.

Ваш исходный класс примет переменную поля типа Formula:

public class MyGreatImpl implements OriginalInterface {
    public var formula:Formula;
    //..
    public function foobar (foo:Rectangle, bar:Array):void {
        for each (var rect:Rectangle in bar) formula.applyFooToBar (foo, rect);
    }
}

Затем вы можете передавать всевозможные формулы - до тех пор, пока они реализуют интерфейс. В результате теперь вы можете использовать интерфейс для создания фиктивных объектов для тестирования всех других частей алгоритма: все, что должен сделать Formula фиктивный объект, - это проверить, что вызван applyFooToBar, и вернуть заданное вами заданное значение за каждое утверждение. Таким образом, вы можете убедиться, что вы действительно не тестируете формулу, например, при тестировании итерации массива.

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

Когда вы разбиваете свой код таким образом, вы можете увидеть, что у вас есть несколько реализаций, которые полагаются на одну и ту же формулу, но имеют разные вариации цикла итерации и т. Д. Это, вероятно, даже позволит вам уменьшить число реализаций вашего начального интерфейса - потому что вариации теперь инкапсулированы в разные классы стратегии.

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

...