Как и зачем писать тесты?
Введение
Начну с конца: приложения нужно тестировать. Точка. А теперь можно и разобраться, как и зачем нам это делать.
Насчет той части, что про “зачем”, можно и коротко:
- чтобы убедиться, что приложение работает так, как задумано
- чтобы отловить ошибку после внесения изменений в код и до того, как код достигнет продакшна
- чтобы зафиксировать идеальное состояние системы — так проще вносить изменения
А по поводу “как” — мы начнем с самого очевидного и самого далекого от разработчиков вида тестирования — тестирования приложения вручную.
Ручное тестирование
Да, приложения тестируют в том числе вручную — руками проверяя все возможные сценарии использования приложения, чтобы удостовериться, что все работает так, как задумано. Вообще, этим занимаются QA-инженеры, но и программистам порой приходится это делать.
Хорошей практикой считается вручную протестить фичу, которую вы написали или баг, который пофиксили. Облегчите работу QA-специалистам, протестируйте то, что написали, прежде чем отдавать код на ревью. В некоторых компаниях такое тестирование даже входит в процесс код-ревью.
Но в целом — статья не об этом, а о разных видах автоматизированного тестирования. Начнем снова с конца: со сквозного тестирования.
Сквозное (E2E) тестирование
Этот подвид автоматизированного тестирования тоже чаще всего берут на себя QA-инженеры. Суть его в том, чтобы полностью эмулировать пользовательскую среду и сымитировать всевозможные пользовательские сценарии автоматически, проверяя, все ли идет так, как должно. Тестирование одновременно и очень полезное — легко отловить ошибку, и очень неточное — даже отловив ошибку, мы не получим никакой информации о том, где именно ошибка произошла.
Для того, чтобы сузить пространство поиска ошибок, существуют другие виды тестов.
Контрактное тестирование (тестирование API-эндпойнтов)
Этот подвид автоматизированного тестирования проверяет, правильно ли работает API. В самом простом случае такое тестирование сводится к запросу к эндпойнту и сравнению ответа с образцом.
Минус такого подхода - оно гарантирует лишь то, что API правильно ответит на запрос. К примеру, мы тестируем POST-эндпойнт, создающий некую сущность в системе. Мы пробуем неправильный запрос — и получаем HTTP 400 Bad Request. Мы отправляем правильно сформированный POST-запрос и получаем HTTP 201 Created. Выглядит так, что все работает отлично, правда? Но на самом деле мы убедились лишь в том, что эндпойнт правильно отреагирует на корректность запроса. Создалась ли сущность в системе после отправки корректного запроса? Мы не знвем.
Есть возможность усовершенствовать этот вид тестирования: помимо проверки ответа API, мы можем сходить в базу и удостовериться, что сущность была создана (удалена, изменена и так далее, зависит от запроса). Такое тестирование дает нам возможность гарантировать правильную работу API, но, во-первых, по сложности приближается к E2E-тестированию, а во-вторых, все еще не очень точно указывает на место, где случилась ошибка, если она случилась.
Для того, чтобы еще ближе приблизиться к месту возникновения ошибки и еще сильнее зафиксировать идеальное состояние системы, существует еще пара видов тестирования, о которых мы поговорим ниже.
Модульное (юнит-тестирование)
Юнит-тестирование — это тестирование самой маленькой части приложения. Обычно это класс, содержащий методы — и вот методы мы и облагаем юнит-тестами. Неоспоримое преимущество такого вида тестирования — если тест падает, мы точно знаем, где именно у нас ошибка. Но с этим тестированием тоже есть проблемка. И заключается она в том, что далеко не всегда наши методы — это чистые функции (об этом подробнее можно почитать в статье о функциональном программировании), и часто они имеют побочные эффекты — вроде обращений к базе данных, внешнему сервису или файловой системе.
Такие обращения принято имитировать (мокать), ведь мы тестируем бизнес-логику, а не слой данных и взаимодействия с внешними сервисами. Для этого мы пишем моки (имитации исходящих взаимодействий) или стабы (имитации входящих взаимодействий) и пробрасываем их в тестируемые методы. Для этого код должен поддерживать инверсию зависимостей и в целом соответствовать правильным практикам написания кода.
Поскольку так происходит не всегда (к сожалению), нам приходится прибегать к интеграционным тестам.
Интеграционное тестирование
В общем и целом, все то, что похоже на юнит-тесты, но:
- захватывает большую часть приложения, чем отдельные методы
- не использует моки/стабы, проверяя интеграции с БД / файловой системой / внешними сервисами
— и называется интеграционными тестами. Этот вид тестирования необходим для проверки взаимодействия разных частей приложения (и поэтому становится очень важным для приложений, построенных с использованием микросервисной архитектуры).
А еще он нередко спасает тогда, когда текущее состояние системы нужно зафиксировать (к примеру, перед масштабным рефакторингом), а качество кода не позволяет обойтись юнит-тестами.
Выводы
Тесты — нужны. Вот вам и выводы :)
А если серьезно, то у каждого вида тестирования есть и плюсы, и минусы. Соблюдайте баланс (тут можно обратиться к пирамиде тестирования), и будет вам счастье :)
Интересный пост?
Вот еще похожие:
- Событийно-ориентированная архитектура и Node.js Events
- Реактивное программирование: теория и практика
- Функциональное программирование. Что это и зачем?
- Профилирование Node.js-приложений
- Docker: что, зачем и почему