Тесты на Codeception для PHP-бэкендов Павел Сташевский, QA-engineer, Lamoda

Мы очень любим и очень давно используем Codeception для тестирования своих проектов (не всех).

Когда-то давно мы сделали перевод документации по Codeception на русский язык (скоро ее обновим).

Еще нам очень нравится движуха, которую устраивает команда Badoo вокруг PHP и его инфраструктуры.

Совсем недавно прошел meetup, на котором рассказывали преимущественно про тестирование.

Своим опытом работы с Codeception поделился Павел Сташевский из Lamoda.

Обязательно посмотрите его выступление.

А мы сделали текстовую расшифровку доклада.

Всем привет! Давайте познакомимся. Меня зовут Паша, я — тестировщик, это нормально, все хорошо. Я буду рассказывать про Codeception, про то, как мы его используем в Lamoda, и как на нем, собственно, мы пишем тесты. Кстати, кто пользуется/пользовался, был опыт использования Codeception?

Ну, есть люди, в общем, это радует. Начнем мы, наверное, как бы немножко с конца, собственно, с PHP-бэкендов, c сервисов, которые мы тестируем с помощью Codeception. В Lamoda много сервисов. Есть сервисы, которые клиентские, которые взаимодействуют непосредственно с нашими пользователями, с пользователями сайта, мобильного приложения. Про них мы говорить не будем. А есть то, что у нас в компании называется глубокие бэкенды, — это наши системы бэк-офиса, который автоматизирует наши бизнес-процессы. Это доставка, это склад, это автоматизация фотостудий, колл-центра. Большинство этих проектов, большинство этих сервисов разрабатывается на PHP, если говорить кратко про стек, это PHP + Symfony, кое-где — старые проекты на Zend’e, в качестве баз данных используется PostgreSQL и MySQL, в качестве систем обмена сообщениями — Rabbit или Kafka.

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

ОК. Поговорили немножко про то, что мы тестируем. Начнем с такого простого, легкого вопроса ­— зачем нам нужны автотесты? Не будем дискутировать, я, пожалуй, сразу изложу свою точку зрения, зачем нам в Lamoda автотесты.

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

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

  1. Не тестировать регресс руками
  2. Быстро релизить
  3. Использовать в качестве документации
  4. Ускорить onboarding новых сотрудников

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

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

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

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

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

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

Почему Codeception?

  1. Можно писать и запускать одинаково тесты любых видов (unit, functional, acceptance)
  2. Многие грабли уе решены, много модулей уже написано
  3. Во всех проектах, несмотря на немного разные потребности, тесты будут выглядеть одинаково

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

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

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

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

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.

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


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


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


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

В этом нет ничего страшного, мы написали свой модуль.

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

Модули для Codeception писать легко. Ок.

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

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

Есть у кого-то потребность запускать shell-команды в тестах? Есть, ура!

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

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

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

  1. Запускать тесты в контейнере, где находится приложение
  2. Запускать тесты в отдельном контейнере, но сделать его таким же как приложение

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

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


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

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

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

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


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

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

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

Если кому-то вдруг надо — заходите, смотрите.

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


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

  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-бандл для фикстур.


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

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

Почему DoctrineFixtureBundle?

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


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

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

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


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

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

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

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

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


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

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

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


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

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

***

ВОПРОСЫ

В: — Вопрос по поводу интеграций. Понятно, что если интеграции есть и они как-то регулируются, мы должны удостовериться, что наш код работает. Если у нас интеграция с внешним сервисом и он не соблюдает какие-то договоренности, но это критический функционал (например, пару лет назад у нас было такое с Яндекс.Деньгами — они меняли формат ответов или принимающегося запроса), что делать в этом случае? Есть ли у вас такие интеграции, как вы их мониторите, тестируете или что-то вроде того?

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

*

В: — Хотелось бы спросить по поводу тестирования UI в связке с бэкендом. Когда мы использовали Codeception для этого, у нас возникли проблемы с использованием PhpBrowser. Ну не проблемы, ожидаемое поведение. Он не умеет исполнять JS, и проверки того, что на странице присутствует какой-то элемент, заканчиваются просто поиском этого элемента в коде HTML-странице. У него может быть просто атрибут “hidden” включен, и тест ничего скажет. Вы как-то сталкивались с такими проблемами? Не используете ли вы тот же самый WebDriver или все на PhpBrowser?

О: — В тех тестах, где нужен Ajax и JS, какие-то проверки, мы используем WebDriver. Там где в UI не надо проверять, а достаточно сделать какое-то действие, без JS, достаточно использовать PhpBrowser.

*

В: — Как составляются сценарии для flow-тестов — на основе статистики, или QA-инженеры, или и то, и то?

О: — Про flow-тесты. В каждом сервисе есть определенные критичные объекты предметной области. В случае управления заказами — это, собственно, заказ. В случае, там, например, системы, которая делает возвраты денег клиентам, — это сам возврат. Для таких критичных объектов пишутся flow-тесты. Они пишутся только на happy path’ы, мы не проверяем здесь негативные сценарии. Нам важно, чтобы для критичных объектов проходил полностью положительный сценарий и мы могли быть уверены в том, что это будет работать. Сценарии для flow-тестов у нас могут писать как тестировщики, так и разработчики — ревью тестов у нас совместное. Если пишутся автотесты, их ревьюят и разработчики, и тестировщики. Если мнения о том, где, что и как нужно проверить, расходятся, это всегда будет обозначено.