Тестируйте не для того, чтобы сделать «зеленые линии», а для того, чтобы убедиться, что ничего не сломается.

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

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

Перед тем, как перейти к примерам, просто помните, что тестирование проводится не для развлечения, а для другого вас… вы устали в 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

Вывод

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