Тестируйте не для того, чтобы сделать «зеленые линии», а для того, чтобы убедиться, что ничего не сломается.
Покрытие — отличный инструмент, позволяющий убедиться, что вы правильно протестировали программное обеспечение. В частности, инструмент покрытия контролирует, какие фрагменты кода затрагиваются при тестировании: в разной степени (в зависимости от возможностей и конфигурации инструмента) проверяются переключатели, классы, строки и т. д. для запуска и выполнения вместе с тестами.
Однако это не означает, что инструмент понимает ни код, ни тест. Есть много тихих проблем, которые ускользают от контроля покрытия. Некоторых можно поймать с помощью определенных инструментов или вариантов покрытия, но лучше понимать недостатки, стоящие за этими неправильными методами, чтобы избегать их в корне.
Перед тем, как перейти к примерам, просто помните, что тестирование проводится не для развлечения, а для другого вас… вы устали в 3 часа ночи, пьяный кодируете вас, год спустя вы и т. д. А также другие члены команды, которые не вы , но кто может быть одной из вышеупомянутых версий вас.
Обратите внимание, что следующие примеры написаны на каком-то псевдокоде, похожем на Python: главное — это концепция, поэтому давайте проигнорируем особенности вашего любимого языка программирования.
Простое-легкое «Не надо!» пример
Мы верим в покрытие. Но самый простой и глупый пример может изменить наше мнение. Давайте возьмем простую функцию, похожую на «hello world»:
function say_hello(String who): if who equals '' || who is null: return "hello who?" return "hello " + who
Теперь предположим, что у нас есть эти тесты:
function test_say_hello: # bad result = say_hello("mock_who") assert type(result) is String function test_say_who: # bad result = say_hello(null) assert type(result) is String
100% покрытие. Все строки, все операторы switch. Аккуратный. Но ущербный.
Действительно, если вы измените реализацию следующим образом, тесты все равно будут проходить. Хотя вы полностью испортили исходную реализацию.
function say_hello(String who): if who is null: return "llama duck" return "hello llama"
Вот почему охвата недостаточно, и к тестированию нужно относиться серьезно.
Пять вещей, которые вы не хотите делать
Я постарался сжать их все в примере, но некоторые конечно не влезли. Вот все примеры один за другим.
1 - Пропустить часть переключателей с несколькими пунктами
Очень глупая ошибка при тестировании — не писать тест для каждого из предложений, составляющих коммутатор. Чтобы сделать практический пример, в приведенном выше примере мы должны были проверить как нулевое, так и пустое условия. Это становится все более и более важным, поскольку переключатели являются вложенными, а состав результатов следует держать под контролем. Иначе баг.
function test_say_hello: # good whom = "world" result = say_hello(whom) assert result equals "hello " + whom function test_say_who_null: # good result = say_hello(null) assert result equals "hello who?" function test_say_who_empty: # good result = say_hello("") assert result equals "hello who?"
То же самое относится к предложениям switch и случаям по умолчанию. В следующем примере вам действительно нужен тест по умолчанию.
function say_hello_switch(String guess_who): switch guess_who: case '' || null: who = "who?" case "Kenobi": who = "General Kenobi" default: who = "world" return "hello " + who function test_say_hello_switch_default: # good whom_arg = "Yoda" result = say_hello(whom_arg) assert result is not null assert result not equals "" assert result not equals "Kenobi" assert type(result) is String assert result equals "hello world"
Примечание. В приведенном выше тесте мы добавили несколько утверждений, чтобы убедиться, что значение, выбранное для whom_arg, подходит для целей тестирования.
2 - Тестирование макета
Здесь нужен немного более сложный пример. Давайте предположим, что у нас есть зависимость от другого программного обеспечения, и вы хотите его смоделировать, чтобы изолировать текущее программное обеспечение для модульного тестирования.
function build_hello_world_page: addressee = "world" content = say_hello(addressee) page = "<html><body>" + content + "</body></html>" return page
Хорошо, теперь нам нужно издеваться; но то, что мы делаем, по ошибке имитирует функцию, которую мы должны протестировать…
mock build_hello_world_page: # SO BAD! return "<html><body>hello world</body></html>" function test_build_hello_world_page(): page = build_hello_world_page() assert page equals "<html><body>hello world</body></html>"
Опять же, это плохая идея. Вы не тестируете часть программного обеспечения, но похоже, что вы тестируете некоторые из них. Любые существенные изменения в программном обеспечении не повлияют на прохождение теста. Конечно, этот пример слишком упрощен, но когда моков много и они внедряются довольно далеко в коде, вероятность этой ошибки не так велика.
3 - Любые совпадения/утверждение только типа
Эта проблема (по крайней мере, половина) преувеличена в первом примере.
function test_say_hello: # bad result = say_hello("mock_who") assert type(result) is String function test_say_who: # bad result = say_hello(null) assert type(result) is String
Если вы тестируете только типы, вы не можете контролировать контент и, во многих случаях, бизнес-логику. Плохо, если вы не делаете это целенаправленно.
function test_say_hello: # good result = say_hello("mock_who") assert result equals "hello mock_who" function test_say_who: # good result = say_hello(null) assert result equals "hello who?"
Другая половина проблемы возникает чаще при выполнении интеграционных тестов. В частности, у вас может возникнуть соблазн выполнить тесты вызова с помощью сопоставителей с «любым», чтобы туда могли поместиться пустые макеты (или что-то еще, что вы выдаете).
mock say_hello(any()): # bad return "hello world" function test_build_hello_world_page(): page = build_hello_world_page() assert page equals "<html><body>hello world</body></html>"
Часто это плохая идея, или, по крайней мере, это может не перехватывать ошибки, связанные с аргументами… и это именно то, что вы хотите делать при интеграционном тестировании.
В примере нет подсказки о конкретном переданном аргументе, поэтому изменение реализации, как следует, проходит тест, все портит.
function build_hello_world_page: addressee = new LifeSupportingPlanet() # aw snap! content = say_hello(addressee) page = "<html><body>" + content + "</body></html>" return page
4 - Копировать Вставить
Это плохо. Полная остановка. Нет оправдания. Две основные причины избегать этого. Всегда будет остаток, который вы должны были изменить. И если в счастливых случаях тест не проходит, в плохих случаях у вас есть пройденный тест на неправильной реализации. Худшее, что когда-либо было, - это скопировать и вставить реализацию, чтобы получить тест. Быстро, аккуратно, 100% покрытие… бесполезно, так как наследует ошибки, которые вы уже заложили в реализации.
5 - Пустое тестирование
Никогда не пишите пустой тест. Всегда. Возьмите 1,4 секунды и поместите утверждение о сбое. Таким образом, звонок вашей мамы, сообщение вашей жены, звонок босса, вспышка сна, тело, что угодно не оставит на месте зеленый тест, ничего не проверяющий.
function test_i_am_leaving_undone: assert false # TODO missing implementation
Вывод
Не стоит недооценивать необходимость тестов. Они являются барьером, спасающим вас от падения и отправки ошибочного кода. Это правда, что иногда они кажутся не очень полезными, а затраты на их реализацию не оправдывают себя, но при изменении кода год спустя или не вами, преимущества огромны. Так что найдите время и инвестируйте в глубокое тестирование, имея надлежащее семантическое покрытие.