Введение

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

Насчет той части, что про “зачем”, можно и коротко:

  • чтобы убедиться, что приложение работает так, как задумано
  • чтобы отловить ошибку после внесения изменений в код и до того, как код достигнет продакшна
  • чтобы зафиксировать идеальное состояние системы — так проще вносить изменения

А по поводу “как” — мы начнем с самого очевидного и самого далекого от разработчиков вида тестирования — тестирования приложения вручную.

Ручное тестирование

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

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

Но в целом — статья не об этом, а о разных видах автоматизированного тестирования. Начнем снова с конца: со сквозного тестирования.

Сквозное (E2E) тестирование

Этот подвид автоматизированного тестирования тоже чаще всего берут на себя QA-инженеры. Суть его в том, чтобы полностью эмулировать пользовательскую среду и сымитировать всевозможные пользовательские сценарии автоматически, проверяя, все ли идет так, как должно. Тестирование одновременно и очень полезное — легко отловить ошибку, и очень неточное — даже отловив ошибку, мы не получим никакой информации о том, где именно ошибка произошла.

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

Контрактное тестирование (тестирование API-эндпойнтов)

Этот подвид автоматизированного тестирования проверяет, правильно ли работает API. В самом простом случае такое тестирование сводится к запросу к эндпойнту и сравнению ответа с образцом.

Минус такого подхода - оно гарантирует лишь то, что API правильно ответит на запрос. К примеру, мы тестируем POST-эндпойнт, создающий некую сущность в системе. Мы пробуем неправильный запрос — и получаем HTTP 400 Bad Request. Мы отправляем правильно сформированный POST-запрос и получаем HTTP 201 Created. Выглядит так, что все работает отлично, правда? Но на самом деле мы убедились лишь в том, что эндпойнт правильно отреагирует на корректность запроса. Создалась ли сущность в системе после отправки корректного запроса? Мы не знвем.

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

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

Модульное (юнит-тестирование)

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

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

Поскольку так происходит не всегда (к сожалению), нам приходится прибегать к интеграционным тестам.

Интеграционное тестирование

В общем и целом, все то, что похоже на юнит-тесты, но:

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

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

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

Выводы

Тесты — нужны. Вот вам и выводы :)

А если серьезно, то у каждого вида тестирования есть и плюсы, и минусы. Соблюдайте баланс (тут можно обратиться к пирамиде тестирования), и будет вам счастье :)