Как написать «хорошие» юнит-тесты?

61

Приведенный в действие этим потоком , я (снова) думаю об окончательном использовании модульных тестов в моих проектах. Несколько постеров там говорят что-то вроде «Тесты - это круто, если они хорошие тесты». Мой вопрос сейчас: что такое "хорошие" тесты?

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

Jens
источник
8
Любой хороший модульный тест должен проверять только одну вещь - если он проваливается, вы должны точно знать, что пошло не так.
Габлин
2
При наличии больших объемов данных хорошо бы написать общие тесты, которые могут принимать файлы данных в качестве входных данных. Файлы данных обычно должны содержать как входные данные, так и ожидаемый результат. С помощью тестовых сред xunit вы можете создавать тестовые примеры на лету - по одному для каждой выборки данных.
froderik
2
@gablin «Если это не удастся, вы должны точно знать, что пошло не так», можно предположить, что тесты с несколькими возможными причинами неудач в порядке, если вы можете определить причину по результатам теста ...?
user253751
Кажется, никто не упомянул, что модульные тесты могут проверить, сколько времени займет операция. Вы можете выполнить рефакторинг своего кода с учетом производительности, гарантируя, что модульный тест покажет вам, прошел ли он или нет на основе времени, а также результатов.
CJ Деннис

Ответы:

52

Искусство модульного тестирования может сказать следующее о модульных тестах:

Юнит-тест должен иметь следующие свойства:

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

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

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

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

Энди Лоури
источник
1
+1 за полный список, предназначенный для модульного тестирования (не интеграции или функционального тестирования)
Гари Роу
1
+1 за ссылку. Интересный материал можно найти там.
Джорис Мейс
1
«Беги быстро» имеет большое значение. Это одна из причин, по которой модульные тесты должны выполняться изолированно от внешних ресурсов, таких как база данных, файловая система, веб-служба и т. Д. Это, в свою очередь, приводит к издевательствам / заглушкам.
Майкл Пасха
1
когда это говорит It should run at the push of a button, означает ли это, что модульное тестирование не должно требовать запуска ни контейнеров (сервера приложений) (для тестируемого модуля), ни соединения с ресурсами (например, БД, внешние веб-службы и т. д.)? Я просто запутался, какие части приложения должны тестироваться модульно, а какие нет Мне сказали, что модульные тесты не должны зависеть от соединения с БД и работы контейнеров и, возможно, вместо этого использовать макеты.
амфибия
42

Хороший модульный тест не отражает функцию, которую он тестирует.

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

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

mojuba
источник
21
+1 В этом случае вы должны проверить жестко закодированные аргументы и проверить свой известный ответ.
Майкл К
Я видел этот запах раньше.
Пол Мясник
Не могли бы вы привести пример хорошего модульного теста для функции, которая возвращает средние значения?
Влас
2
@VLAS тестирует предопределенные значения, например, убедитесь, что avg (1, 3) == 2, а также, что более важно, проверьте граничные случаи, такие как INT_MAX, нули, отрицательные значения и т. Д. Если в функции была обнаружена и исправлена ​​ошибка, добавьте еще одну проверьте, чтобы эта ошибка никогда не появлялась
Моджуба
Интересно. Как вы предлагаете получить правильные ответы на эти входные данные теста, а не делать ту же ошибку, что и код, подвергнутый тесту?
Тимо
10

Хорошие юнит-тесты - это, по сути, спецификация в работоспособном виде:

  1. описать поведение кода, соответствующего сценариям использования
  2. охватывать технические угловые случаи (что происходит, если передается нулевое значение) - если тест для углового случая отсутствует, поведение не определено.
  3. перерыв, если тестируемый код отличается от спецификации

Я обнаружил, что Test-Driven-Development очень хорошо подходит для библиотечных подпрограмм, поскольку вы сначала пишете API, а затем - реальную реализацию.


источник
7

для TDD «хорошие» тесты тестируют функции, которые хочет заказчик ; функции не обязательно соответствуют функциям, и тестовые сценарии не должны создаваться разработчиком в вакууме

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

Пример истории:

Как [X-Wing Pilot] я хочу [не более 0,0001% погрешности посадки], чтобы [целевой компьютер мог попасть в выпускной порт Звезды Смерти при движении на полной скорости через каньон коробки]

Итак, вы идете поговорить с пилотами (и с целевым компьютером, если разум). Сначала вы говорите о том, что является «нормальным», затем говорите о ненормальном. Вы узнаете, что действительно имеет значение в этом сценарии, что является общим, что маловероятным, а что просто возможно.

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

//Scenario 1 - can you hit the side of a barn?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is zero
    and all other values are constant,
Then:
    the error coefficient must be zero

//Scenario 2 - can you hit a turtle?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is less than c
    and all other values are constant,
Then:
    the error coefficient must be less than 0.0000000001/ns

...

//Scenario 42 - death blossom
Given:
    all 7 channels with 30% dropout and a 0.05 second sampling window
When:
    speed is zero
    and position is within enemy cluster
    and all targets are stationary
Then:
    the error coefficient must be less than 0.000001/ns for each target

Теперь вы, возможно, заметили, что нет сценария для конкретной ситуации, описанной в истории. После разговора с заказчиком и другими заинтересованными сторонами выясняется, что цель в оригинальной истории была лишь гипотетическим примером. Настоящие испытания вышли из последовавшего обсуждения. Это может случиться История должна быть переписана, но это не обязательно [так как история - просто место для разговора с клиентом].

Стивен А. Лоу
источник
5

Создайте тесты для угловых случаев, например, набор тестов, содержащий только минимальное количество входов (возможно 1 или 0) и несколько стандартных случаев. Эти юнит-тесты не являются заменой для тщательных приемочных испытаний и не должны быть ими.

user281377
источник
5

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

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

Кроме того, я не верю в написание тестов просто ради того, чтобы сказать: «Да, мы тестируем это». Если я использую библиотеку, которая вставлена ​​и останется неизменной, я не собираюсь тратить день на написание тестов, чтобы убедиться, что внутренности API, которые никогда не изменятся, работают должным образом, даже если определенные его части оцениваются высоко на графе вызовов. Тесты , которые потребляют указанную библиотеку (мой собственный код) указать на это.

Тим Пост
источник
но что будет позже, когда в библиотеке будет более новая версия с исправлением ошибки?
@ Thorbjørn Ravn Andersen - Это зависит от библиотеки, что изменилось и их собственного процесса тестирования. Я не собираюсь писать тесты для кода, который, как я знаю, работает, когда я его уронил, и никогда не трогал. Так что, если после обновления все работает, то с ума сходит :) Конечно, есть исключения.
Тим Пост
если вы зависите от вашей библиотеки, как минимум вы можете сделать , это написать тесты , которые показывают , что вы ожидаете , сказали библиотека на самом деле сделать ,
... и если это изменится, тестирует вещи, которые используют указанную библиотеку ... tl; dr; Мне не нужно проверять внутренности стороннего кода. Ответ обновлен для ясности, хотя.
Тим Пост
4

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

glenatron
источник
3

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

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

Я часто использую макеты. Хорошие фреймворки, вероятно, были бы весьма полезны, такие как PowerMock. Хотя я пока не пользуюсь.

Если класс A использует другой класс B, я бы добавил интерфейс X, чтобы A не использовал B напрямую. Затем я бы создал макет XMockup и использовал его вместо B в своих тестах. Это действительно помогает ускорить выполнение теста, уменьшить сложность теста, а также уменьшает количество тестов, которые я пишу для A, так как мне не приходится справляться с особенностями B. Я могу, например, проверить, что A вызывает X.someMethod () вместо побочного эффекта вызова B.someMethod ().

Держите тестовый код в чистоте.

При использовании API, такого как уровень базы данных, я смоделировал бы его и включил макет, чтобы вызвать исключение при каждой возможности команды. Затем я запускаю тесты один без броска, и в цикле, каждый раз выбрасывая исключение при следующей возможности, пока тест не завершится снова. Немного похоже на тесты памяти, доступные для Symbian.

Роджер К.С. Вернерссон
источник
2

Я вижу, что Андри Лоури уже опубликовал метрики модульных тестов Роя Ошерова; но, похоже, никто не представил (бесплатный) набор, который дядя Боб дает в Чистом коде (132-133). Он использует аббревиатуру ПЕРВЫЙ (здесь с моими резюме):

  • Быстро (они должны бежать быстро, чтобы люди не возражали против них)
  • Независимо (тесты не должны делать настройку или демонтаж друг друга)
  • Повторяется (должен работать на всех средах / платформах)
  • Самопроверка (полностью автоматизирована; вывод должен быть либо «пройден», либо «провал», а не файл журнала)
  • Своевременно (когда писать их - непосредственно перед написанием производственного кода, который они тестируют)
Kazark
источник