Привет! Меня зовут Гриша и я бэкенд разработчик на .net  С друзьями мы часто обсуждаем процессы разработки, в том числе как пишутся автотесты. Я часто слышу о подходах, которые несут боль разработчикам и одновременно с этим приносят мало пользы.  При этом я строю весь процесс разработки микросервиса на тестах. Я бы хотел поделиться своим взглядом на то, как писать компонентные тесты и в чём они могут быть полезны помимо проверки работы сервиса. Давайте посмотрим на место компонентных тестов в пирамиде тестирования, зачем они нужны и кому. Поговорим про то какими свойствами должны обладать идеальные тесты. Рассмотрим подход к написанию обладающих всеми этими свойствами тестов и напишем парочку. Все знают что любая статья про тестирование должна начинаться с пирамиды. Я не верю в пирамиду тестирования в микросервисах, поэтому вместо неё будет табличка. еnd2еnd  Компонентные  Интеграционные Модульные Что тестируют группу сервисов сервис в изоляции  часть сервиса, либо весь сервис с замокаными зависимостями класс или метод Скорость медленно медленно медленно быстро Кто пишет скорее тестировщики но могут и разработчики скорее разработчики но могут и тестировщики разработчики разработчики Где живут в отдельном проекте в проекте с кодом в проекте с кодом в проекте с кодом Блокируют ли пайплайн в ci нет да да да Удобно ли дебажить нет, только remote debug да да да Когда требуются правки поменялся контракт поменялся контракт поменялась реализация поменялась реализация  Могут ли проверять негативные сценарии нет да да да Генерируют тестовые данные на машине разработчика нет да нет нет Полезны для воспроизведения багов нет да  скорее нет  скорее нет Поднимают окружение для разработки нет да нет нет Модульные тесты быстрые, простые, но ломаются при рефакторинге. Обычно их пишут для алгоритмов или каких-то других сложных участков кода. Я их пишу на доменные объекты, если сервис на ДДД. Если сервис - это перекладывание json из одного места в другое, то могу и не писать вообще. Интеграционные тесты - что-то что большее чем Unit тест, но меньше чем тест, покрывающий весь микросервис. Это тесты на репозитории в связке с БД, тесты на сервис, но с замоканными зависимостями - обычно они проверяют взаимодействие нескольких частей проекта, пайплайн запроса.   Я такие тесты не пишу за редкими исключениями - например, когда в БД идут сложные запросы и их надо потестить отдельно. Компонентные тесты тестируют сервис целиком, на реальных с точки зрения сервиса зависимостях (Мы не вмешиваемся в конфигурацию DI сервиса и не подсовываем туда заглушки - сервис шлёт настоящие запросы и получает настоящие ответы). В идеале такие тесты также документируют требования, которые ставились перед его разработкой, и гарантируют что сервис соответствует им.  Я стараюсь писать их всегда. End2End тесты проверяют связку сервисов - обычно простые позитивные сценарии. Если все сервисы в связке покрыты компонетными тестами, то e2e по сути проверяет лишь, что контракт не изменился, и связка сервисов может работать вместе. Я редко пишу эти тесты - обычно, когда сомневаюсь, что вторая сторона не поменяет наш контракт. На практике в сервис добавляются только те тесты, которые нужны - в моём случае это был бы скорее ромб тестирования. Есть алгоритмы - есть юнит тесты. Есть сложная структура из микросервисов - неплохо бы написать е2е сценарий. Вот только компонентные тесты я стараюсь писать всегда и много. И главная причина для этого - последние 3 пункта в табличке. Я пробегусь по каждому: Поднимают окружение для разработки - запустил тесты, они прошли или упали - у тебя уже все что нужно для сервиса поднято локально в докере - база, RabbitMq или Kafka, http моки - все что вообще можно поднять там. Это удобно. Генерируют тестовые данные на машине разработчика - нужно подебажить как работает код для сущности, которая зависит от другой, которая зависит от третей и так далее - запусти подходящий тест, который в своих шагах эти сущности сгенерит. И да - он будет - потому что на все CRUD операции такие тесты пишутся. Это супер удобно. Полезны для воспроизведения багов - тестировщик вместо текстового пошагового описания бага делает мерж реквест с тестом, который не проходит. Ты запускаешь его и дебажишь. Это супер мега удобно - по крайней мере для разработчика 🙂 Ближе к делу! Давайте покроем типовой микросервис компонентными тестами и на практике посмотрим, удобно это или нет. Схема теста у нас будет следующая  Перед запуском тесткейсов поднимем всё необходимое сервису окружение в докере - контейнер с базой данных, брокером сообщений и прочим. Если у нас есть какие-то синхронные взаимодействия - поднимем контейнер для динамического создания заглушек сервисов. В нашем случае это Mountebank - а заглушки там называются imposter. Перед каждым тесткейсом будем очищать состояние всех зависимостей от артефактов, которые могли там остаться от предыдущих кейсов. Каждый тесткейс будет состоять из 3 этапов: Given - конфигурация окружения. Этот этап не обязательный. Обычно там формируются импостеры с требуемым поведением для прохождения тесткейса. When - инициирующее действие. Обычно это запрос в сервис, получение ответа и запись ответа в общую переменную. Then - проверка результата. Обычно это проверка http ответа + проверка изменений, требуемых по контракту. Например, проверка того, что в RabbitMQ выгрузилось сообщение и оно корректно. Также в этом методе извлекаются необходимы для дальнейших шагов данные и помещаются в общие переменные. Например, можно извлечь id созданной сущности чтобы потом проверить её обновление и удаление. Реализуем эту схему в коде на примере сервиса часто задаваемых вопросов FAQ: Сервис должен предоставлять API для создания категорий и вопросов.  Он должен уметь искать по тексту вопроса. Сервис должен быть закрыт авторизацией через jwt токен. При создании вопроса мы должны отправить его в сервис модерации через RabbitMQ. Вот код контроллеров сервиса.   Вам кажется что код плохой и его надо переписать? Вам не кажется. Но прежде чем рефакторить микросервис, мы же должны покрыть его тестами, чтобы быть уверенными, что мы ничего не сломаем 🙂 Создадим проект с для компонентных тестов со следующими зависимостями  Теперь давайте создадим наш микрофреймворк, который будет состоять из 4 файлов:  При запуске тестов мы первым делом попадаем сюда. Сначала мы запускаем внешнее окружение для сервиса, затем перед каждым сценарием очищаем состояние в зависимостях.  Класс представляет собой внешнее окружение. При запуске тестов удаляет все запущенные контейнеры, запускает нужные сервису и дожидается пока они поднимутся. В нашем случае это 3 контейнера: PostgresContainer для базы данных сервиса RabbitmqContainer для шины данных, в которую отправляет сообщение сервис MountebankContainer для создания http моков - нам он пригодится чтобы замокать keyCloak Также здесь устанавливаются переменные окружения и запускается сервер с приложением.  Базовый класс для контейнеров зависимостей. Содержит данные об образе и декларирует методы для запуска и конфигурации контейнера.  Класс содержит данные, которые должны передаваться между шагами в сценарии, а также метод для их очистки - чтобы один сценарий никак не мог влиять на другой. Теперь давайте напишем наш первый сценарий.  Сами шаги опишем в соответствующих файлах   Я подробно прокомментировал шаги и не думаю, что тут есть что добавить. Но хотел бы обратить внимание на тот факт, что благодаря тому, что наши тесты живут в том же репозитории и написаны на том же языке программирования, мы можем переиспользовать DTO модели из проекта сервиса. Это удобнее, чем работать с json как текстом или писать свои модельки для тестов. Наш первый сценарий готов. Ура!  В нашем сервисе реализована авторизация. Чтобы запрос проходил, мы должны с одной стороны сгенерировать и добавить в него валидный JWT токен, с другой стороны подготовить инфраструктуру, чтобы сервис мог проверить валидность этого токена. Давайте вспомним как работает JWT авторизация. KeyCloak не так просто поднять в контейнере и корректно настроить. Мы не будем это делать - вместо этого мы его замокаем. Как видно из схемы, наша заглушка keyCloak должна уметь обрабатывать 3 запроса: Получение токена. Получение настроек проверки токена. Получение сертификата для проверки токена. Создадим шаг с типом Given - как мы помним, именно такие шаги нужны для настройки окружения. В нём с помощью Mountebank сгенерируем нужный нам импостер.  Также нам нужен шаг, в котором клиент получит токен и сохранит его для дальнейших запросов.  Теперь добавим блок Background, который будет выполняться перед каждым сценарием. Вот теперь наш сценарий полностью готов! Mountebank поддерживает и другие протоколы - grpc, websockets и т.д. Таким образом этот подход позволяет замокать любое синхронное взаимодействие - главное, чтобы мы могли реализовать нужную логику генерации ответов. Как видно из кода шага Given - в нашем случае логика генерации ответов keyCloak инкапсулирована в класс KeyCloakResponseGenerator.  Методы GetOpenidConfiguration и GetCertificates содержат  статический контент. Они создавались просто - я перехватил ответы из тестового KeyCloak и по ним сгенерил модельки. Обычно сервисы-зависимости на определённый запрос генерируют определённый статический ответ - поэтому проблем в генерации заглушек на такие зависимости не возникает. В нашем случае все немного сложнее. Нам необходимо чтобы мы могли добавлять разные клеймы в токен и подписать его. К тому же если бы мы просто перехватили токен с теста и захардкодили его - он бы быстро протух и перестал работать. Поэтому в нашем случае токен генерируется руками с помощью JwtTokenBuilder  Этот класс содержит методы для генерации токена с нужными клеймами, а также приватный ключ, который позволяет подписать сгенерированный токен. Публичная часть ключа, которая позволяет проверить подпись, отдаётся в методе GetCertificates. Ухх, это было сложно, но мы справились, осталось совсем немного. Давайте напишем сценарий для вопросов. Поскольку вопрос нельзя создать без категории, добавим блок Background, который сгенерирует нужные данные в сервисе (про авторизацию тоже не забудем). Здесь я бы хотел заострить внимание на удобстве переиспользования шагов для создания нужного состояния сервиса. У нас в блоке Background всего ничего - 4 строки. Но в реальных системах со сложными схемами данных, таких зависимостей может быть намного больше. И это очень удобно, когда ты в любой момент с помощью теста можешь сгенерировать любые нужные тебе данные в сервисе - удобно не только для написания тестов, но и для воспроизведения и разбора различных багов. Напишем чуть более продвинутый сценарий на проверку поиска вопросов, чтобы показать удобство и возможности языка Gherkin. Он позволяет как передавать табличные значения в метод, так и выполнять проверку по таблице результатов.  Вот код метода, который принимает табличные данные  Таким образом мы довольно компактно написали тест на проверку разнообразных вариантов поиска вопросов. Я слышал мнение что Grerkin не удобен для разработчиков, многословен и что проще писать все тесты просто в коде. Я не согласен с этим мнением - плохо себе представляю как этот тест можно было бы столь же компактно и наглядно реализовать в коде. Знакомство с языком Grerkin и specflow можно продолжить тут https://specflow.org/ Перейдём к финальной части нашей статьи - тестированию отправки сообщения в RabbitMQ.  Напишем вот такой простой сценарий Чтобы проверить, что вопрос успешно отправлен, нам нужно прочитать его из очереди RabbitMQ - это поможет сделать самописный клиент для REST API  А сам шаг проверки будет выглядеть так  Ну вот мы и добрались до конца статьи. Мне кажется получилось хоть и объёмно, но достаточно просто. Я специально написал комментарии почти на каждую строку, чтобы любой тестировщик, даже без знаний c#, мог освоить такой подход к написанию тестов. Весь проект с тестами можно посмотреть вот здесь https://gitlab.com/grisha0088/ComponentTestsExample У нас в команде принято так - разработчик пишет базовые кейсы на сервис, а тестировщик уже обогащает их. В таком случае, особенно если разработчик готов помогать, даже тестировщики без опыта автоматизации начинают такие тесты писать. В целом я почти перестал вручную отправлять запросы в сервис. Через тесты удобно проверять работу сервиса, воспроизводить баги, дебажить - для меня они стали не только инструментом проверки качества - они полностью встроились в процесс разработки и эксплуатации сервиса. Мне не нравится складывающаяся в последнее время в индустрии практика, когда сервисы покрываются End2End тестами, обычно написанными на Python, которые живут в отдельном репозитории и полностью разрабатываются силами тестировщиков. Такие тесты могут проверять качество сервиса, но на этом их польза заканчивается. Да и эксплуатировать их - дебажить если тест упал, следить чтобы на них точно не забили - дело не простое. Я не против End2End тестов, но, как мне кажется, не они должны лежать в основе - их роль, базовые сценарии в интеграции микросервисов. Я написал эту статью, чтобы показать, что есть отличная альтернатива End2End тестам и ничего сложного в таком подходе нет. Надеюсь, эта статься поможет вам писать действительно полезные тесты и получать радость от работы с ними 🙂