Мы часто экспериментируем с архитектурой, кодом, производительностью. Постоянно добавляем новый функционал. Мы постепенно обвязываем Yii своей “архитектурной” прослойкой — шардинг, работа с временно недоступными данными, разнообразные кеши и многое другое. Да, плод нашей работы, когда он будет заврешен, пойдет в Open Source.
Задача применяемой у нас Непрерывной Интеграции (Continuous Integration, CI) — не тестирование. Задача CI — обезопасится от разрушительных изменений в следствие рефакторинга, добавления нового функционала, изменений архитектуры. Также мы защищаемся от “плохого кода”, часто повторяющихся багов, “кривых” merge.
Для своего CI мы используем Jenkins под Debian. Время на развертку CI я затратил 12 часов — до полностью рабочего состояния. На поддержку CI я не трачу ни минуты в день — я не пишу тесты на каждую мелочь, не практикую TDD. Тем не менее, CI работает и спасает нас от глупых ошибок.
“Давайте будем внимательней”/”Давайте не делать ошибок” — взывал я к разработчикам, но это помогало лишь временно и то не на все 100%. Людям свойственно ошибаться, забывать, совершать оплошности. Нет, я не изобрел “серебряную пулю” для web-проектов и даже маленьку пульку для Yii — я придумал как стабилизировать свое приложение. Ваше приложение отличается от моего и мои методы у Вас могут не работать, да и не должны — я же делал их не для Вашего приложения, если мои методы работаю у Вас — примите это как чудо или как везение. Зато идея такого CI будет работать везде. Всего лишь идея.
В чем идея
Идея в том, чтобы регрессивно проверять приложение на предмет “отвалившегося” функционала без затраты N часов на тесты в день. Добиться этого просто — если написать один тест на “абстрактную сущность” — то тест должен проходить со всеми её “конкретными” имплементациями. Если стандартизировать код — сделать разные его части имплементацией нескольких абстракций, например трех — весь код можно будет покрыть 3мя проверками. Да, именно “проверками”, а не тестами — я не тестирую функционал, я проверяю, что “код работает, не падает, не фаталит”. При правильном коде редко ломается бизнес-логика так, чтобы не вызвать фатальной ошибки. По крайней мере у нас. Мы стараемся писать код так — если логика работает верно — она работает, если нет — кидается FatalException или происходят другие фатальные ошибки. Я считаю этот “жесткий” путь верным, т.к. иначе будет очень сложно искать сломавшуюся логику.
Мы стандартизировали код до следующих абстракций: модель (она уже вполне стандартна в Yii и имеет вполне понятный интерфейс с методами find, save, delete). контроллер (он тоже вполне стандартный), экшен (action), компонент, библиотека.
Если с моделями все просто, то с контроллерами и экшенами пришлось повозится. Мы решили, что любой внешний вызов (http, console) не должен вызывать фатальной ошибки (http >= 500): нет сущности — значит 404, кривой запрос — значит 400, нет доступа — 403. Если Ваш контроллер фаталит при обращении к нему с несуществующей id или с какими либо еще кривыми параметрами — это неверное поведение с точки зрения протокола http — ошибка пользователя — это 4xx, а не 5xx — не нужно фаталить при кривых запросах, нужно давать пользователю осмысленную ошибку “что он делает не так”.
Собственно проверка контроллеров и была построена по этому принципу — конструируем модуль, контроллер, дергаем action — смотрим что произошло — ExceptionPage404 — это нормально (данные то мы в $_GET не передали), а вот если FatalException или PHP Error — это уже плохо — тест не прошел.
Компоненты для Yii, которые мы пишем мы тоже стандартизировали. В нашем случае любой компонент — это расширение существующего функционала Yii. Например, добавление шардинга, глобальный кеш БД. Такой функционал одновременно имплементирует две абстракции — модель Yii и наш компонент раширения. Проверяется он тоже 2 раза — на примере всех моделей, и отдельно как компонент.
Библиотеки — это написанный нами совсем “левый” функционал, не имеющий отношения к Yii, а реализующий только какой-то частный случай логики, чаще всего — взаимодействие с другими сервисами нашей сервис-ориентированной архитектуры. Проверки и тесты на них — тема для отдельной статьи, скажу лишь одно — мы проверяем их как отдельные проекты в нашем CI и производим “интеграционные тесты” внутри основного приложения.
Реализация на нашем примере
У нас 4 шага сборки:
- Деплой, миграции, установка/обновление зависимостей — она не имеет отношения к описанной выше идее, просто скажу что оно есть.
- Проверка качества кода
- Интерфейсная проверка кода — имплементация описанной выше идеи.
- Небольшое количество Unit-тестов на часто всплывающие баги (как просто phpunit, так и selenium+phpunit). Они крайне редко “поддерживаются” или добавляются — поэтому я наприсал “не трачу N часов в день на тесты” — я трачу от силы 1 час в месяц на написание 1 теста, на 1 надоевший баг.
Шаг первый — деплой
Выполняется проверка в 2х вариантах — миграция с предыдущей версии (идентичной текущему состоянию продакшина) и развертка с нуля (с автоматизированной установкой виртуалных машин, автоматической конфигурацией puppet, разверткой приложения и базы)
Ничего конкретного не скажу, т.к. это не касается данной статьи и совсем другая история.
Шаг второй — качество кода
Первое что мы проверяем: “php -l” — все ли у нас парсится — без этого дальнейшее не имеет смысла. Второе что мы ищем в коде — это запрещенные вызовы: die, var_dump, ini_set, exit. Потом ищем с помощью обычного fgrep последствия кривого мержа: “<<<<<<<”, “>>>>>>”, “======” — такой мусор временами проскакивает, когда мержили руками и одно конфиликтное место не заметили и не разрешили конфликт.
Так же мы ищем с помощью регулярных выражений следующее:
- Методы длинной в много экранов кода.
- Слишком вложенный код вида “5 вложенных if”
- Слишком “загруженный код” вида print(preg_replace(‘/@/’, “/%/”, “a”.substr(1, 5, $lala).(int)(bool)$d)); — сложно читать, сложно писать и не хочется смотреть на такой код.
Шаг третий — интерфейсная проверка кода
Он разбит на несколько “подшагов” — проверка моделей, контроллеров, компонентов, универсальный selenium-проверки (да, и такое тоже есть! чуть ниже расскажу), интеграционные тесты с библиотеками.
Подробно расскажу о самом простом и самом интересном. Самое простое — модели.
Любая модель должна: сохранятся, выбиратся, удалятся. Она именно для этого и существует. Специально для этого теста мы добавили в каждую модель статический метод, создающий “дефолтную валидную модель” — модель которую можно создать, сохранить, удалить из БД, проходящую валидацию.
На самом деле мы не писали в 250 моделях 250 методов для создания таких моделей. Мы написали один метод у родителя — он вычленяет из rules параметры вылидации и заполняет поля валидными значениями. Потратил я на это — 2 часа.
В итоге для каждой модели в цикле мы делаем примерно следующее:
$model = ModelClass::createDefault(); //создаем дефолтную модель
$this->assertNotNull($model); //создалась?
$this->assertTrue($model->save()); //сохранилась?
$pk = $model->getPk(); //выбираем primary key
$loaded = ModelClass::model()->findByPk($pk); //ищем модель по этому primary key
$this->assertNotNull($loaded); //нашли?
$this->assertTrue($model->delete()); //удаляется?
$this->assertIsNull(ModelClass::model()->findByPk($pk)); //удалили?
Этой не хитрой проверкой мы сделали следующее: убедились, что шардинг прослойка работает, кеш БД не мешает нормальной работе, таблица в БД нормально смигрировала и эта модель в эту таблицу сохраняется (+ тригеры не падают).
Самое интересное же — selenium проверки.
Мы изучили наш интерфейс и пришли к радостному выводу — он вполне стандартизирован. Есть 4 основных варианта взаимодействия с пользователем:
- Смена глобальной страницы
- Смена Таба
- Открытие диалогового окна
- Отправка формы в диалоговом окне
Первые три пункта автоматизировались крайне легко — теги A и Button меняющие глобальную страницу имеют css-класс global, меняющие таб — имеют атрибут data-tab, открывающие диалог — имеют атрибут data-msgbox. Нам было достаточно сделать 3 вложенных цикла: меняет страницу (тупо кликает на кнопку), меняет таб (тоже жмет на кнопку), открывает диалог (и тоже просто нажатие). На каждом из вложенных этапов мы проверяем — изменился ли контент страницы, поменялся ли контент в div-е для табов, открылся ли диалог. Попутно собираем из браузера возникшие ошибки js.
С формами было несколько хитрее. Нам пришлось добавить к элементам формы атрибуты data-type со значениями возможных валидных данных — data-type = “email”, “anyString”, “checkboxChecked”, “phone”, “anyFile”, и другие. И вот! Формы стандартизированы и мы имеем общий интерфейс для всех input-ов — в поля с валидными email мы заполняем email, в поля с phone — телефон, и так по всем полям. Отправляем форму и проверяем, что диалог закрылся без ошибок — значит данные сохранились. Потом повторяем все тоже самое с невалидными данными, например в поле email пишем телефон — и проверяем что форма не отправилась, а ошибка для пользователя появилась.
На добавление атрибутов в поля форм я потратил около 1,5 часа. И у нас не мало форм — просто это простая работа и если сесть и сделать — то это не долго.
Таким вот нехитрым (а может и хитрым) методом мы проверили весь UI на предмет:
- Фаталов при открытии страниц, табов, окон
- Фаталов при отправке валидных и невалидных форм.
- Что формы сохраняются с валидными данными
- Что формы выдают ошибки с не валидными данными.
Юнит-тесты и selenium-тесты
Честно скажу — их мало, очень мало. Их мы добавляем только тогда когда появился повторявшийся баг и в очередной раз тестировщики сказали “ну вот снова не работает!”
Мы никогда не меняем старые, написанные тесты — мы разрабатываем приложение с учетом обратной совместимости. Это нужно не только ради тестов — приложение имеет API для мобилок/десктопов и оно обязано быть обратно совместимым.
И что дальше?
Чуть позже мы стандартизировали наш js-код и покрыли его (спасибо за testit товрищу titulusdesiderio — мы адаптировали его под запуск из под nodejs и там тестируем свой js)
Еще позже мы покрыли тестами css+html верстку — проверяли развалившуюся верстку с помощью diff-а скриншотов, на предмет кривизны.
Обо всем этом я расскажу отдельно, если Вам конечно интересно.
P.S. Прежде чем ругать с фразами “это не тестирование”, “оно покрывает 5% функционала” и подобным: мы не тестируем. Мы именно делаем проверку работоспособности.
Это как проверка того, что лампочка горит в магазине — мы не проверяем её нагрев, не меряем излучаемый свет, не пробуем вкрутить в неподходящий патрон — мы проверяем, просто что она горит. Тоже самое мы делаем и с кодом. Простым, и не требующим поддержки способом.
Автор: piromanlynx