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

Вам знакомо следующее?

Я не писал тесты, потому что у меня не было времени.

Я не писал тесты, потому что у клиента не было на это бюджета.

Я не писал тесты, потому что мне нужно было сразу же запустить его, и как только это было сделано, какой смысл писать тесты дальше ??

Я не писал тесты, потому что изначально бизнес-логика была непонятной.

И мой любимый:

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

Даже если вы напишете тесты, головоломки продолжатся:

Стоит ли сначала писать тесты?

Что на самом деле означает TDD?

Какой процент тестов должен быть модульным?

Какой процент тестов должен быть функциональным?
Это не может быть мой код, потому что я написал модульные тесты.

Тесты прошли, так что все должно быть в порядке.

Я не закончил с билетом, потому что я писал тесты в течение последних 2 недель.

Между двумя известными компьютерными учеными, Джимом Коплиеном и Робертом Мартином ведутся интересные дискуссии о разработке через тестирование (TDD). Оба опубликовали несколько бестселлеров; Джим Коплиен считает TDD вуду, а дядя Боб клянется им.

Даже те, кто считает тестирование необходимостью, не могут прийти к согласию по некоторым фундаментальным вопросам. Следует ли имитировать все взаимодействия с базой данных? Что на самом деле означает "функциональный тест"? Что считается «хорошо протестированным программным обеспечением» и как мы можем это измерить? Что на самом деле означает покрытие кода? Какой процент покрытия кода является чрезмерным, а какой следует считать минимальным? Должен ли каждый запрос на вытягивание содержать тесты? Если да, то какие тесты? На сегодняшний день нет консенсуса и четких доказательств в поддержку какой-либо одной доктрины. На протяжении своей карьеры мне посчастливилось работать с множеством кодовых баз, от тех, в которых почти не было автоматических тестов, до других, в которых были тысячи. Я видел обе крайности в успешных компаниях со здоровой прибылью и довольными клиентами.

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

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

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

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

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

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

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

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

Итак, работает ли это для каждого отдельного приложения?
Конечно, нет!

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

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

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

Редакционные обзоры на Деанна Чоу, Карло Джентиле, Небез Брифкани, Лиела Туре и Пратик Саньял

Хотите работать с нами? Нажмите здесь, чтобы увидеть все открытые вакансии на SSENSE!