Как проверить, когда расположение данных слишком громоздко?

19

Я пишу парсер, и как часть этого, у меня есть Expanderкласс, который «расширяет» одно сложное утверждение в несколько простых операторов. Например, это расширило бы это:

x = 2 + 3 * a

в:

tmp1 = 3 * a
x = 2 + tmp1

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

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

Или я мог бы написать это как строку и разобрать это:

var input = new Parser().ParseStatement("x = 2 + 3 * a");

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

Мой вопрос: можно ли в основном (или полностью) полагаться на такого рода интеграционные тесты для тестирования этого Expanderкласса?

svick
источник
3
То, что ошибка Parserможет провалиться в каком-то другом тесте, не является проблемой, если вы обычно делаете коммит только при нулевых сбоях, напротив, это означает, что у вас больше охвата Parser. То, о чем я бы предпочел беспокоиться, это то, что ошибка Parserмогла сделать этот тест успешным, если он не прошел . В конце концов, есть модульные тесты, чтобы найти ошибки - тест не работает, но он должен быть.
Йонас Кёлькер

Ответы:

27

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

var input = new Parser().ParseStatement("x = 2 + 3 * a");

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

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

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

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

вид теста, который тестирует на самом низком уровне. Таким образом, вы получите хороший микс: несколько самых простых, примитивных тестов (чистые юнит-тесты), но не потратили недели на написание тестов на этом примитивном уровне. Это дает вам время, необходимое для написания гораздо большего количества чуть менее атомарных тестов с Parserиспользованием помощника. Конечный результат: больше тестов, больше охвата, больше углов и других интересных случаев, лучший код и более высокая гарантия качества.

Джонатан Юнис
источник
2
Это разумно, особенно если учесть, что все зависит от многих других. Хороший модульный тест должен проверить минимально возможный. Все, что находится в пределах этого минимально возможного количества, должно быть проверено предыдущим модульным тестом. Если вы полностью протестировали Parser, вы можете предположить, что вы можете безопасно использовать Parser для тестирования ParseStatement
Jon Story
6
Основная проблема чистоты (я думаю) состоит в том, чтобы избежать написания циклических зависимостей в ваших модульных тестах. Если в тестах синтаксического анализатора или синтаксического анализатора используется расширитель, и этот тест расширителя основан на работе анализатора, то у вас есть сложный для управления риск того, что все, что вы тестируете, состоит в том, что анализатор и расширитель согласованы , тогда как Вы хотели проверить, действительно ли расширитель делает то, что должен . Но до тех пор, пока нет обратной зависимости, использование парсера в этом модульном тесте ничем не отличается от использования стандартной библиотеки в модульном тесте.
Стив Джессоп
@ SteveJessop Хороший вопрос. Важно использовать независимые компоненты.
Джонатан Юнис
3
Что-то, что я сделал в случаях, когда сам синтаксический анализатор является дорогостоящей операцией (например, чтение данных из файлов Excel с помощью com-взаимодействия), - это написание методов генерации тестов, которые запускают анализатор и выводят код на консоль, воссоздают структуру данных, возвращаемую парсером , Затем я копирую выходные данные генератора в более обычные модульные тесты. Это позволяет уменьшить перекрестную зависимость, поскольку анализатор должен работать правильно только тогда, когда тесты создавались не каждый раз, когда они запускаются. (Не тратить несколько секунд / тест на создание / уничтожение процессов Excel было хорошим бонусом.)
Дэн Нили
+1 за подход @ DanNeely. Мы используем нечто подобное для хранения нескольких сериализованных версий нашей модели данных в качестве тестовых данных, чтобы мы могли быть уверены, что новый код все еще может работать со старыми данными.
Крис Хейс
6

Конечно, все в порядке!

Вам всегда нужен функциональный / интеграционный тест, который выполняет полный путь кода. И полный путь к коду в этом случае означает в том числе оценку сгенерированного кода. То есть вы проверить , что синтаксический x = 2 + 3 * aпроизводит код, если запустить с a = 5установит xна 17и при запуске с a = -2задаст xв -4.

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

Ян Худек
источник
4

Модульные тесты позволяют вам определить конкретные элементы, которые ломаются и где в коде они сломались. Так что они хороши для очень мелкозернистого тестирования. Хорошие юнит-тесты помогут сократить время отладки.

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

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

Питер Смит
источник
0

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

Тулаинс Кордова
источник