Как подойти к юнит-тестированию и TDD (используя Python + нос) - PullRequest
7 голосов
/ 11 марта 2012

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

  1. Идея TDD состоит в том, что модульные тесты пишутся перед кодом, который они тестируют. Модульное тестирование должно проверять небольшие части кода (например, функции), которые для целей теста являются автономными и изолированными. Однако мне кажется, что это сильно зависит от реализации. Во время реализации или во время более позднего исправления может возникнуть необходимость абстрагировать часть кода в новую функцию. Должен ли я затем пройти все свои тесты и смоделировать эту функцию, чтобы сохранить их изолированными? Конечно, при этом существует опасность появления новых ошибок в тестах, и тесты больше не будут тестировать точно такую ​​же ситуацию?

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

  3. Часто, будучи изолированными, модульные тесты просто повторяют функцию. Например. если есть простая функция, которая добавляет два числа, то тест, вероятно, будет выглядеть примерно так: assert add(a, b) == a + b. Поскольку реализация просто return a + b, какой смысл в тесте? Гораздо более полезным тестом было бы посмотреть, как работает функция в системе, но это противоречит модульному тестированию, поскольку оно больше не изолировано.

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

Итак, мои основные вопросы:

  1. Должны ли модульные тесты использоваться повсеместно, как бы ни была мала и проста эта функция?
  2. Как справиться с изменением реализаций? То есть Должна ли реализация тестов постоянно меняться, и это не уменьшает их полезность?
  3. Что делать, когда тест усложняется по сравнению с кодом, в котором он тестируется?
  4. Всегда ли лучше начинать с юнит-тестов, или лучше начинать с системных тестов, которые в начале разработки гораздо проще написать?

Ответы [ 2 ]

5 голосов
/ 11 марта 2012

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

  1. Я считаю полезным рассматривать каждую функцию как API в этом контексте. Модульный тест - это тестирование API, а не реализация. Если реализация изменится, тест должен остаться прежним, это сеть безопасности, которая позволяет вам с уверенностью рефакторинг вашего кода. Даже если рефакторинг означает перевод части реализации в новую функцию, я скажу, что можно оставить тест таким, какой он есть, без заглушки или насмешек над частью, которая была подвергнута рефакторингу. Однако вам, вероятно, понадобится новый набор тестов для новой функции.
  2. Модульные тесты не являются святым Граалем! Тестовый код должен быть довольно простым, на мой взгляд, и не должно быть особых причин для сбоя самого тестового кода. Если тест становится более сложным, чем тестируемая функция, это, вероятно, означает, что вам необходимо изменить рефакторинг кода по-другому. Пример из моего прошлого: у меня был некоторый код, который брал некоторый ввод и выводил некоторый вывод, сохраненный как XML Синтаксический анализ XML для проверки правильности вывода привел к большим сложностям в моих тестах. Однако, понимая, что XML-представление не имело значения, я смог провести рефакторинг кода, чтобы можно было проверить вывод, не вмешиваясь в детали XML.
  3. Некоторые функции настолько тривиальны, что отдельный тест для них не добавляет никакой ценности. В вашем примере вы на самом деле не тестируете свой код, но оператор «+» на вашем языке работает так, как ожидалось. Это должно быть проверено разработчиком языка, а не вами. Однако эта функция не должна сильно усложняться перед тем, как добавить тест для нее.

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

4 голосов
/ 12 марта 2012

1) Должны ли юнит-тесты использоваться повсеместно, как бы ни была мала и проста эта функция? Нет. Если в функции нет логики (если while-циклы, добавления и т. Д.), Тестировать нечего. Это означает, что функция добавления реализована следующим образом: def add(a, b): return a + b У него нет ничего для проверки. Но если вы действительно хотите построить для него тест, то: assert add(a, b) == a + b # Worst test ever! - худший тест, который когда-либо мог написать. Основная проблема заключается в том, что проверенная логика НЕ ​​должна воспроизводиться в коде тестирования, поскольку: Если там есть ошибка, она будет также воспроизведена. Вы больше не тестируете функцию, но a + b работает одинаково в двух разных файлах. Так что было бы больше смысла что-то вроде: assert add(1, 2) == 3 Но, опять же, это всего лишь пример, и эту add функцию даже не нужно тестировать. 2) Как справиться с изменением реализаций?

Это зависит от того, какие изменения. Имейте в виду, что:

  • Вы тестируете API (грубо говоря, что для данного ввода вы получаете определенный результат / эффект).
  • Вы не повторяете рабочий код в своем тестовом коде (как описано выше).

Таким образом, если вы не измените API своего производственного кода, тестовый код не будет затронут каким-либо образом.

3) Что делать, когда тест усложняется по сравнению с кодом, в котором он тестируется?

Кричи на тех, кто написал эти тесты! (И переписать их).

Модульные тесты просты и не содержат никакой логики.

4a) Всегда ли лучше начинать с юнит-тестов или лучше начинать с системных тестов?

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

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

4b) Что в начале разработки гораздо проще написать?

Модульные тесты! Поскольку у вас даже нет корня вашего кода, как вы можете писать системные тесты?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...