Хабрахабр

Тесты на Codeception для PHP-бэкендов

Всем привет! Меня зовут Паша, и я QA инженер команды Order Processing в Lamoda. Недавно я выступал на PHP Badoo Meetup. Сегодня хочу представить расшифровку своего доклада.

Речь пойдет про Codeception, про то, как мы его используем в Lamoda и как на нем пишем тесты.

В Lamoda много сервисов. Есть сервисы клиентские, которые взаимодействуют непосредственно с нашими пользователями, с пользователями сайта, мобильного приложения. Про них мы говорить не будем. А есть то, что у нас в компании называется глубокие бэкенды — это наши системы бэк-офиса, которые автоматизируют наши бизнес-процессы. К ним относятся доставка, склад, автоматизация фотостудий и колл-центра. Большинство этих сервисов разрабатываются на PHP.

Кое-где старые проекты на Zend’e. Если говорить кратко про наш стек, то это PHP + Symfony. В качестве баз данных используется PostgreSQL и MySQL, в качестве систем обмена сообщениями — Rabbit или Kafka.

Почему PHP-бэкенды?

Если у них есть UI, то это UI больше вспомогательный, который используют наши внутренние пользователи. Потому что у них, как правило, развесистый API — это либо REST, кое-где есть чуть-чуть SOAP’а.

Зачем нам в Lamoda автотесты?

Не будем вручную ничего регрессионно тестировать. Вообще, когда я пришел работать в Lamoda, там был такой лозунг: “Давайте избавимся от ручного регресса”. Собственно, вот одна из главных причин, зачем нам нужны автотесты — чтобы не гонять регресс руками. И мы работали над этой задачей. Правильно, чтобы быстро релизить. А зачем нам это нужно? Это, наверное, самые главные цели. Чтобы мы могли безболезненно, очень быстро выкатывать наши релизы и при этом иметь некоторую сетку из автотестов, которые нам будут говорить, хорошо или нехорошо. Но есть еще парочка вспомогательных, про которые я тоже хочу сказать.

Зачем нужны автотесты?

  1. Не тестировать регресс руками
  2. Быстро релизить
  3. Использовать в качестве документации
  4. Ускорить onboarding новых сотрудников
  5. Автотесты удобно использовать (в некоторых случаях) в качестве документации. Иногда проще зайти в тесты, посмотреть, какие кейсы покрыты, как они работают, и понять, как работает тот или иной функционал, и ускорить вхождение новых сотрудников — как разработчиков, так и тестировщиков, — в новый проект. Когда садишься писать автотесты, сразу становится понятно, как работает система.

Ок, поговорили о том, зачем нам нужны автотесты. Теперь поговорим о том, какие тесты мы пишем в Lamoda.

image

Про нижние два уровня я говорить не буду, не зря они таким белым цветом закрашены. Это достаточно стандартная пирамида тестирования, начиная от unit-тестов, заканчивая E2E-тестами, где тестируются уже некоторые бизнес-цепочки. Тестировщик, в крайнем случае, может зайти в Pull Request, посмотреть код и сказать: “Ну, что-то тут недостаточно кейсов, давайте покроем еще что-нибудь”. Это тесты на сам код, их пишут у нас разработчики. На этом работа тестировщика для этих тестов заканчивается.

Начнем с системных тестов. Мы будем говорить про уровни выше, которые у нас пишут и разработчики, и тестировщики. Как правило, эти тесты достаточно атомарные. Это тесты, которые тестируют API (REST или SOAP), тестируют некую внутреннюю логику систем, различные команды, разборы очередей в Rabbit или обмен с внешними системами. Например, один API-метод или одну команду. Они не проверяют какую-то цепочку, а проверяют одно действие. И проверяют как можно на большем количестве кейсов, как позитивных, так и негативных.

Я их поделил на 2 части. Идем дальше, E2E-тесты. И есть тесты, которые мы называем flow-тесты. У нас есть тесты, которые тестируют связку UI и бэкенда. Они тестируют цепочку — жизнь объекта от начала до конца.

Внутри такой системы может быть тест — заказ от создания до доставки, то есть прохождение его по всем статусам. Например, у нас есть система управления процессинга нашими заказами. Вы сразу видите весь flow определенных объектов, с какими внешними системами все это взаимодействует, какие команды для этого используются. Именно по таким тестам потом очень легко и просто смотреть, как работает система.

Мы не гоняем на каких-то фермах эти тесты, нам достаточно проверить в одном браузере, а иногда даже не нужно использовать браузер. Поскольку у нас этим UI пользуются внутренние пользователи, нам не важна кросс-браузерность.

“Почему Codeception мы выбрали для автоматизации тестирования?” — наверное, спросите вы.

Когда я пришел в Lamoda, Codeception уже был выбран как стандарт, чтобы писать автотесты, и я столкнулся с ним по факту. Если честно, у меня нет ответа на этот вопрос. Этим я и хочу с вами поделиться. Но, поработав какое-то время с этим фреймворком, я все-таки понял, почему Codeception.

Почему Codeception?

  1. Можно писать и запускать одинаково тесты любых видов (unit, functional, acceptance).
  2. Многие грабли уже решены, много модулей уже написано.
  3. Во всех проектах, несмотря на немного разные потребности, тесты будут выглядеть одинаково.
  4. Концепция Codeception предполагает, что на этом фреймворке вы пишете любые тесты: unit, интеграционные, функциональные, приемочные. И они у вас, по крайней мере, будут одинаково запускаться.
  5. Codeception — это достаточно мощный комбайн, в котором уже решено много проблем, много вопросов, много задач для тестов. Если что-то не решено, то скорее всего, вы найдете что-то извне — некоторый аддон для какой-то специфической работы. Вам не нужно писать какие-то тестовые обертки для баз данных, для еще чего-то. Просто берете и подключаете к Codeception модули и работаете с ними.
  6. Ну и такой плюс (наверное, он больше подходит для больших компаний, когда у вас много проектов и сервисов) — во всех проектах тесты будут выглядеть плюс-минус одинаково. Это очень здорово.

Кратко скажу, что из себя представляет Codeception, поскольку многие с ним работали.

image

После того, как вы его затягиваете в проект и инициализируете, генерируется такая структура. Codeception работает по модели акторов.

В них создается конфигурация ваших тестов. У нас есть yml-файлы, вот там снизу — functional.suite.yml, integration.suit.yml, unit.suite.yml. Есть папочки под каждый вид тестов, где эти тесты лежат, есть 3 вспомогательных папочки:

_data — для тестовых данных;
_output — куда кладутся отчеты (xml, html);
_support — куда кладутся какие-то вспомогательные хелперы, функции и все, что вы напишите, чтобы использовать в ваших тестах.

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

Стандартные модули

  1. PhpBrowser
  2. REST
  3. Db
  4. Cli
  5. AMQP

Первый такой модуль — это PhpBrowser. Этот модуль — обертка над Guzzle, который позволяет взаимодействовать с вашим приложением: открывать странички, заполнять формы, сабмитить формы. И если вам не важно кроссбраузерное и браузерное тестирование, если вы вдруг тестируете UI, можно использовать PhpBrowser. Как правило, в наших UI-тестах мы его и используем, потому что нам не нужно какой-то сложной логики взаимодействия, нам достаточно открыть страничку и что-то небольшое там сделать.

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

В последних версиях Codeception туда добавлена поддержка не одной, а нескольких баз данных. Третий модуль, который мы используем из коробки, — это модуль Db. Поэтому, если вдруг у вас в проекте несколько баз данных, теперь это работает из коробки.

Модуль Cli, который позволяет запускать shell — и bash-команды из тестов, и мы его тоже используем.

Хочу заметить, что официально он протестирован на RabbitMQ. Есть модуль AMQP, который работает с любыми брокерами сообщений, которые основаны на этом протоколе. Поскольку мы используем RabbitMQ, у нас с ним все окей.

Но над кое-чем все-таки пришлось поработать. На самом деле, Codeception, по крайней мере, в нашем случае покрывает 80-85% всех нужных нам задач.

Начнем с SOAP.
image

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

Это означает, что нельзя в Codeception-модуле это так сконфигурировать в yml-файле, чтобы работать сразу с несколькими.
image У нас, например, есть монолиты, у которых несколько WSDL, несколько SOAP-эндпоинтов.

В данном случае необходимо подменять эндпоинт и используемую схему. У Codeception есть динамическая реконфигурация модуля, и вы можете написать какой-то свой адаптер, чтобы получать, например, модуль SOAP и динамически его реконфигурировать. Тогда в тесте, если вам нужно поменять эндпоинт, на который вы хотите отправить запрос, получаем наш адаптер и меняем на новый эндпоинт, на новую схему и отправляем на нее запрос.
image

В этом нет ничего страшного, мы написали свой модуль.
image В Codeception нет работы с Kafka и нет никаких сторонних более-менее официальных аддонов, чтобы работать с Kafka.

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

Вывод: модули для Codeception писать легко.

Как я уже сказал, в Codeception есть модуль Cli — обертка для shell-команд и работы с их output’ом.
image Идем дальше.

Вообще тесты и приложения — это немного разные сущности, они могут лежать в разных местах. Но иногда shell-команду нужно запустить не в тестах, а в приложении. Тесты могут запускаться в одном месте, а приложение может быть в другом.

Так зачем нам нужно запускать shell в тестах?

Эти команды в прод-режиме запускаются из-под супервизора. У нас есть в приложениях команды, которые, например, разбирают очереди в RabbitMQ и двигают объекты по статусам. Если они упали, то он их заново запускает и так далее. Супервизор следит за их выполнением.

Иначе тесты становятся нестабильными, непредсказуемыми. Когда мы тестируем, супервизор у нас не запущен. Поэтому нам нужно из тестов запустить эти команды в приложении. Мы сами хотим управлять запуском этих команд внутри приложения. Что один, что другой — в принципе, все то же самое, и все работает. У нас используются два варианта.

Как запускать shell в приложении?

Поскольку все приложения у нас в Docker’е, тесты можно запускать в том же контейнере, где находится сам сервис. Первое: запускать тесты в том же самом месте, где находится приложение.

То есть из того же Docker-образа, и тогда все будет работать аналогично.
image Второй вариант: делать под тесты отдельный контейнер, некоторый test runner, но делать его таким же, как приложение.

Ниже пример того, с чем можно и нужно работать. Еще одна задача, с которой мы столкнулись в тестах — это работа с различными файловыми системами. Это Webdav, SFTP и амазоновская файловая система. Для нас актуальны первые три.

С чем нужно работать?

  1. Webdav
  2. FTP/SFTP
  3. AWS S3
  4. Local
  5. Azure, Dropbox, google drive

Если порыться в Codeception, можно найти какие-то модули практически для любой более-менее популярной файловой системы.

image

Но эти файловый системы плюс-минус одинаковые в плане внешней работы с ними, и нам хочется работать с ними одинаково. Единственное, что я не нашел, это для Webdav’a.

Он лежит на Github в открытом доступе и поддерживает 2 файловые системы — SFTP и Webdav — и позволяет работать с обеими по одинаковому API.
image Мы написали свой модуль, который называется Flysystem.

Если туда добавить еще и амазоновскую файловую систему, наши потребности точно покроются. Получить список файлов, почистить директорию, записать файл, и так далее.

Вообще, хочется чтобы было, как на картинке, — ВЖУХ и все завелось, заработало, и вот эти базы данных в тестах меньше бы поддерживать.
image Следующий момент, я считаю, очень важный для автотестов, тем более системного уровня, — это работа с базами данных.

Какие я вижу здесь основные задачи:

  1. Как раскатать БД нужной структуры — Db
  2. Как заполнить БД тестовыми данными — Db, Fixtures
  3. Как делать выборки и проверки — Db

Для всех 3-х задач в Codeception есть 2 модуля — Db, про который я уже говорил, другой называется Fixtures.

Из этих 2 модулей и 3 задач мы используем только DB для третьей задачи.

Там можно сконфигурировать SQL-дамп, из которого будет разворачиваться база данных, ну и модуль с фикстурами, думаю, понятно, зачем нужен. Для первой задачи можно использовать DB.

Там будут фикстуры в виде массивов, которые можно персистить в базу данных.

Как я сказал, первые 2 задачи мы решаем немного по-другому, сейчас я расскажу, как мы это делаем.

Разворачивание БД

  1. Поднимаем контейнер с PostgreSQL или MySQL
  2. Накатываем все миграции с помощью doctrine migrations

Первое — про разворачивание базы данных. Каким образом у нас происходит это в тестах. Мы поднимаем контейнер с нужной базой данных — либо PostgreSQL, либо MySQL, потом накатываем все нужные миграции с помощью doctrine migrations. Все, база данных нужной структуры готова, ее можно использовать в тестах.

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

Мы не используем модуль Fixtures от Codeception, мы используем Symfony-бандл для фикстур.
image Второй момент — создание тестовых данных.

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

У вас фикстура тогда будет создаваться как некоторый объект предметной области, ее можно заперсистить в базу данных, и тестовые данные будут готовы.

Почему DoctrineFixtureBundle?

  1. Проще создавать цепочки связанных объектов.
  2. Меньше дупликации данных, если фикстуры для разных тестов похожи.
  3. Меньше правок при изменении структуры БД.
  4. Фикстуры-классы гораздо нагляднее чем массивы.

Почему мы его используем? Да по той же причине — эти фикстуры гораздо проще поддерживать, чем фикстуры от Codeception. Проще создавать цепочки связанных объектов, потому что это все заложено в Symfony-бандл. Нужно меньше дублировать данные, потому что фикстуры можно наследовать, это классы. Если меняется структура базы данных, эти массивы всегда нужно править, а классы — не всегда. Фикстуры в виде объектов предметной области всегда нагляднее, чем массивы.

Про базы данных поговорили, поговорим немного про моки.

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

Правила для моков

  1. Мокаем все внешние http-взаимодейтвия сервиса
  2. Проверяем не только позитивные, но и негативные сценарии

Взаимодействия — это некоторые http-взаимодействия по REST или SOAP. Все эти взаимодействия в рамках тестов мы мокируем. То есть у нас в тестах нигде не идет реального обращения к внешним системам. Это делает тесты стабильными. Потому что внешний сервис может работать, может не работать, может медленно отвечать, может быстро, в общем, неизвестно, какое у него поведение. Поэтому мы все это покрываем моками.

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

Каким образом он работает? Для моков мы используем Wiremock, сам Codeception поддерживает…, у него есть такой официальный аддон Httpmock, но Wiremock нам понравился больше.

Wiremock поднимается как отдельный Docker-контейнер во время тестов, и все запросы, который должны идти ко внешней системе, идут на Wiremock.

image

Все очень просто: пришел запрос — получил мок. У Wiremock, если посмотреть на слайд — там есть такой квадратик, Request Mapping, у него есть набор таких маппингов, которые говорят о том, что, если пришел такой запрос, надо отдать такой ответ.

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

Здесь приведен пример, как создать мок динамически, вы видите, описание достаточно декларативное, из кода сразу понятно, что за мок мы создаем: мок для метода GET, который придет на такой URL, и, собственно, что вернуть.

image

Это тоже бывает очень полезно в тестах. Кроме того, что этот мок можно создать, у Wiremock есть возможность потом еще и проверить, какой запрос ушел на этот мок.

Про сам Codeception, наверное, все, и несколько слов о том, как запускаются наши тесты, и немного инфраструктурщины.

Что у нас используется?

image

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

Для внутренних команд используется Make, в качестве CI используется Bamboo.

Как выглядит запуск тестов на CI?

image

Сначала мы билдим нужную версию приложения, потом поднимаем окружение — это приложение, все сервисы, которые ему нужны, вроде Kafka, Rabbit, база данных и на базу данных мы накатываем миграцию.

Именно в CI, на проде все контейнеры крутятся под Kubernetes. Все это окружение поднимается с помощью Docker Compose. Затем запускаем тесты и прогоняем.

Сколько времени это все занимает?

Все зависит от конкретного сервиса, но, как правило, подъем окружения до запуска тестов — это 5-10 минут, тесты — от 6 до 30 минут.

image

Сразу предупрежу этот вопрос, пока все тесты гоняются в одном потоке.

Как часто надо запускать тесты? Ну и такой вопрос. Чем раньше вы сможете поймать проблему, тем быстрее вы сможете ее решить. Конечно, чем чаще, тем лучше.

Когда задача переходит в тестирование, на ней должны проходить все тесты, и unit, и не unit-тесты. У нас есть 2 главных правила. Если какие-то тесты не проходят, это повод перевести задачу в фиксинг.

На релизе все тесты должны обязательно проходить. Естественно, когда мы выкатываем релиз.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть