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

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

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



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

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

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

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

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

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

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

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

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

    image

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

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

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

    Например, у нас есть система управления процессинга нашими заказами. Внутри такой системы может быть тест — заказ от создания до доставки, то есть прохождение его по всем статусам. Именно по таким тестам потом очень легко и просто смотреть, как работает система. Вы сразу видите весь 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. Думаю, из названия понятно, что он делает. Для любых http-взаимодействий можно использовать этот модуль. В нем, мне кажется, решены практически все взаимодействия: хедеры, cookie, авторизация. Все, что нужно, в нем есть.

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

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

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

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

    Начнем с SOAP.
    image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    image

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

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

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

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

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

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

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

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

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

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

    Как я сказал, первые 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, база данных и на базу данных мы накатываем миграцию.

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

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

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

    image

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

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

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

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

    В конце мне хотелось бы сказать что-то воодушевляющее — пишите тесты, пусть они будут зелеными, используйте Codeception, делайте моки. Думаю, вы все это прекрасно понимаете.
    • +29
    • 8,5k
    • 6
    Lamoda
    50,94
    Russian Fashion Tech
    Поделиться публикацией

    Похожие публикации

    Комментарии 6

    • НЛО прилетело и опубликовало эту надпись здесь
        0
        Поскольку это высокоуровневые тесты, то мы проверяем логику работы с RabbitMQ/Kafka в рамках конкретных бизнес-процессов.
        Как правило, это два сценария — на чтение и запись:
        1. Положить валидное сообщение в очередь/топик, прочитать его. Или положить невалидное сообщение (без какого-то обязательного поля, просто мусор и т.д.), проверить как его обработает приложение.
        2. Выполнить операцию внутри приложения, которая что-то пишет в очереди/топики, и проверить, что все сгененерировались корректно.
        0
        Спасибо за доклад.
        Вы упомянули модуль Db, а также то, что используете Doctrine.
        Но не упомянули модуль Doctrine2 (https://codeception.com/docs/modules/Doctrine2).
        Вы его не используете? Если нет, почему?
          0
          Мы его не используем, в нем не очень удобно персистить в базу связанные объекты, особенно если их много. И опять же все свойства для объектов нужно передавать через массив.
          В случае DoctrineFixturesBundle получается более читабельный код.
          0
          Можете уточнить по поводу раскатки базы данных — она делается один раз перед запуском тестов на bamboo? Данные от предыдущих тестов в одном запуске не мешают?
          Как выглядит запуск одного-двух тестов в окружении разработчика, как разворачивается бд в этом случае?
            0
            Локально и на bamboo у нас одна схема работы с БД:
            1. Поднимаем контейнер с БД.
            2. Раскатываем структуру и справочники миграциями. База раскатана и больше не перекатывается.
            3. Перед каждым тестом чистим таблички и заполняем нужными данными с помощью DoctrineFixturesBundle. Каждый тест получается атомарным, друг на друга никак не влияют.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое