Монолит для сотен версий клиентов: как мы пишем и поддерживаем тесты
Темы:
- Наш процесс разработки
- Юнит-тесты
- Интеграционные тесты
- Тесты на API
- Прогон тестов
Процесс разработки
К сроку выкатывания фичи добавляется:
- desktop web + два месяца
- mobile web + неделя
- iOS + месяц
- android + полгода
Разработчик отвечает за фичу от бэкенда до реализации на платформах. Интеграция бывает нескоро. Поэтому хочется сделать так, чтобы интеграция прошла хорошо.
Нужны автотесты!
Unit-tests
Тесты на изолированные кусочки кода.
Сложно тестировать легаси. Для него делаем SoftMocks. Оно перехватывает все include/require в PHP-файле, подменяет подключаемый файл на другой.
Можно мокать любые методы: статические, приватные, финальные.
Проблема: SoftMocks расслабляют — можно писать плохо тестируемый код и всё равно покрыть его тестами. Решили правилами:
- новый код должен быть легко тестируемым с помощью PHPUnit
- SoftMocks — крайний случай, когда иначе нельзя.
Правила проверяются на код-ревью.
Мутационное тестирование.
- берем код
- берем code coverage
- парсим код и генерируем мутации: меняем плюс на минус, true на false
для каждой мутации прогоняем набор тестов:
- если тесты упали — они хорошие
- если тесты успешно прошли — они недостаточно эффективны
Для PHP есть готовые решения: Humbug, Infection. Но они несовместимы с SoftMocks. Поэтому написали своё.
Интеграционные тесты
Тестируем работу нескольких компонентов в связке.
Стандартный подход:
- Поднимаем тестовую БД
- Заполняем её
- Запускаем тест
- Очищаем БД
Есть проблемы:
- нужно поддерживать актуальность содержимого базы
- требуется время на подготовку
- параллельные запуски делают тесты нестабильными и порождают дедлоки (когда тесты работают с одной и той же таблицей)
Решение: DBMocks
- Методы драйверов DB перехыватываются с помощью SoftMOcks на setUp теста
- из запроса вытаскиваются db + table
- создаются временные таблицы с такой же схемой
- все запросы идут во временные таблицы
- потом на tearDown они удаляются
Результаты:
- тесты не могут повредить данные в оригинальных таблицах
- тестируем совместимость запроса с версией MySQL.
- тесты вообще ходят в ту же БД по тому же адресу (просто таблица другая)
- тесты изолированы друг от друга
API-тесты
- имитируют клиентскую сессию
- умеют слать запросы к бэкенду
- бэкенд отвечает почти как реальному клиенту
Обычно такие тесты требуют авторизованного пользователя. Его нужно создать перед тестом и удалить после. Это порождает риск нестабильности, потому что при создании пользователя есть репликация и фоновые задачи.
Решение: пул тестовых пользователей.
- Нужен пользователь
- ищем в пуле
- если нет — создаём
- после теста очищаем, потому что тест загрязняет пользователя
Тестовые пользователи в одном окружении с реальными? Зачем вообще их тестировать на бою? Потому что devel ≠ prod. Надо изолировать, иначе пользователи будут флиртовать с кучей фиктивных Олегов.
Путь 1: флаг is_test_user
, по флагу изолируем внутри сервисов.
Путь 2: всех пользователей переселяем в условную Антарктиду и они там тусят друг с другом.
QA API
бэкдор для тестирования
- хорошо документированные методы API
- быстро и легко управляют данными
- пишут их бэкенд-разработчики
- применимы только к тестовым пользователям
Позволяют сменить пользователю неизменяемые данные, вроде даты регистрации.
Безопасность
- сетевая изоляция — доступно только из офисной сети
- с каждым запросом передаётся secret, валидность которого проверяем
- методы не работают с реальными пользователями
- программа BugsBounty на hackerone
Прогон тестов
- 100 000 юнит, 40 минут
- 6 000 интеграционных, 90 минут
- 14 000 API, 600 минут
Распараллелили тесты в TestCloud.
Как распределить тесты между потоками?
- поровну — фигня, все тесты разные, получатся неравные по времени части
- запустить несколько потоков и скармливать тесты по одному — тратим время на инициализацию PHP-окружения. Долго!
Как сделали:
- собираем статистику по времени прогона
- компонуем тесты так, чтобы один chunk прогонялся не более чем за 30 секунд
Проблема: медленные тесты занимают ресурсы, не давая выполняться быстрым. Т.е. API могут занять весь Cloud.
Решение: разделили Cloud
- часть 1 прогоняет только быстрые тесты
- часть 2 прогоняет любые тесты
Результат:
- Unit — 1 минута
- Интеграционные — 5 минут
- API — 15 минут
Какие тесты выполнять? Покажет code coverage.
- Получаем branch diff
- формируем список измененных файлов
- получаем список тестов, которые его покрывают
- запускаем прогон набора только из этих тестов
А где взять coverage?
- раз в сутки, ночь, полный прогон с test coverage
- результаты складываем в БД
Плюсы:
- прогоняем меньше тестов: меньше нагрузка на железо и быстрее обратная связь
- можем запускать тесты для патчей. Это позволяет быстро выкатить хотфикс. В патчах скорость важнее всего.
Минусы:
- Релизим бэкенд дважды в день, так что coverage актуален только для первого. Поэтому для билда прогоняем полный набор тестов.
- API-тесты генерируют слишком большой coverage. Для них этот подход не даёт большой экономии.
Итоги
- все уровни пирамиды тестирования нужны и важны
- количество тестов ≠ качество. Делайте ревью кода тестов и мутационное тестирование.
- изолируйте тестовых пользователей от реальных.
- бэкдоры в бэкенде упрощают и ускоряют разработку тестов.
- собирайте статистику по тестам.
Ссылки
- Слайды: bit.ly/yants-HL18
- Наш протокол: «Как мы поддерживаем 100 разных версий клиентов в Badoo» / Ярослав Голуб
- SoftMocks: «SoftMocks: наша замена runkit для PHP 7» / Юрий Насретдинов, github.com/badoo/soft-mocks
- Badoo BugsBounty
- UI тесты: «Cross Platform Mobile Test Automation and Continuous Delivery» / Sathish Gogineni
- TestCloud: «Оптимальная параллелизация юнит- тестов или 17000 тестов за 4 минуты» / Илья Кудинов