Хабрахабр

Три года автотестов: как повысить скорость и не только

Когда я пришел в Skyeng, здесь решали, стоит ли тратить время на систему автотестов и попросили меня поделиться опытом с предыдущей работы. Привет, я Алексей, full-stack разработчик платформы Vimbox. В итоге я сделал небольшую внутреннюю презентацию, рассказывающую о граблях, на которые успел наступить за несколько лет разработки этих автотестов, борьбы за их скорость, читабельность кода и общую эффективность. А такой опыт у меня был: к моменту ухода с предыдущего места мы написали на php и крутили больше 3 тысяч тестов. Презентация показалась коллегам полезной, поэтому я переложил ее в текст, чтобы оказаться полезным также и более широкой аудитории.

Для начала – термины, о которых пойдет речь в статье:

  • Приемочный тест – end-to-end тест: здесь браузер или эмулятор браузера исполняет сценарий
  • Модульный тест (юнит тест) – тест метода
  • Функциональный тест – тест контроллера или компонента, если речь о фронтенде
  • Фикстура – состояние тестового окружения, необходимое для работы теста (глобальные переменные, данные в БД и прочие участники сценария теста)

Плюсы и минусы разных видов тестов

Приемочные тесты

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

Модульные тесты

  • Плюсы: просто писать, срабатывают быстро. Покрывают небольшой кусок кода, не нужно много состояний, следовательно, не нужна и большая фикстура.
  • Минусы: неустойчивы к изменениям архитектуры или внутренней структуры кода. Если нужно слить два метода в один или разделить, выделить класс, удалить метод, тесты приходится переписывать.

Функциональные тесты – промежуточное решение.

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

Борьба за скорость

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

Опыт перый: моки

Можно настраивать, что будут возвращать методы мока, можно проверять, какие методы мока сколько раз с какими параметрами были вызваны Мок в PhpUnit – динамически создаваемый объект, класс которого динамически наследуется от пародируемого класса.

Подменяя службу моком, мы избавляемся от необходимости думать, что там происходит, разрабатывать дополнительные сценарии и фикстуры, чтобы все корректно заработало. Главный плюс моков – они позволяют отрезать целые куски функциональности. В итоге: меньше фикстур, а скорость срабатывания выше за счет того, что мы отрезали лишний код, выполняющий запросы к БД.

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

Мы должны в ходе теста создать мок-объект и подумать, какие должны быть у него вызваны методы. Минус: код теста слишком привязан к реализации.

Они «не замечают» даже изменения интерфейса, не говоря уж о реализации. Второй обнаруженный минус состоит в том, что тесты стали менее надежны. мы где-то удаляли метод и спустя длительное время обнаруживали, что покрывающие его тесты по-прежнему работают как ни в чем не бывало, потому что видели его мок, а тот делал вид, что все хорошо. Т.е.

Считаю опыт с моками неудачным в плане ускорения тестов.

Опыт второй: SQLite

Пришлось написать транслятор PostgreSQL схeмы в SQLite, после каждой миграции генерировали новую SQLite схему. Следующий вариант – СУБД SQLite, она умеет создавать БД в оперативной памяти. Такой подход поднял скорость тестов на локальных машинах в два-четыре раза. Тесты из этой схемы создавали пустую БД в оперативной памяти. Стало реально прогонять весь комплект тестов несколько раз в час.

Мы потеряли многие нативные возможности PostgreSQL (json, некоторые удобные агрегатные функции и прочее). Но были и минусы. Запросы пришлось писать так, чтобы они срабатывали и на PostgreSQL, и на SQLite.

Опыт третий: оптимизация PostgreSQL

В определенный момент мы узнали, что PostgreSQL можно оптимизировать для автотестов, что сокращает время срабатывания примерно в четыре раза. Это решение было рабочим, но вызывало некоторую боль. Для этого надо добавить несколько настроект в postgresql.conf:

fsync=off
synchronous_commit=off
full_page_writes=off

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

Если получается локализовать базы в отдельный кластер и отключить в нем fsync – это очень удобно. Такая настройка применяется для всего кластера, затрагивает все БД, ее нельзя применить для какой-то одной базы.

Немного о new

Службы, созданные с его помощью, невозможно подменить моками и стабами. Отдельно хочется упомянуть опасность оператора new. Вывод:

  • Не использовать new для создания объектов, которые по своей сути являются службами.
  • Можно использовать в фабриках, потому что их можно будет подменить. Но сами фабрики не должны создаваться через new.
  • Можно использовать для создания моделей, сущностей, DTO (data transfer object), value-objects.

Выводы из трехлетнего опыта

  • На прошлой работе мы отказались от приемочных тестов, но сейчас я бы их снова попробовал: скорее всего в веб-драйверах пофиксили многие баги.
  • Если нужно покрыть новый функционал тестами, надо писать только функциональные тесты контроллеров/компонентов. В этой ситуации у нас высок риск структурных изменений, модульные тесты к ним неустойчивы.
  • Таких тестов не должно быть много, потому что много == медленно, они срабатывают не так быстро, как модульные. Покрывать стоит только те случаи, которые могут «выстрелить» (имеют вероятность ошибки в будущем).
  • Модульные тесты пишутся на алгоритмически-насыщенные методы (сложная логика, которую надо тестить) или на методы с небольшим риском структурных изменений в будущем.
  • Минусы моков в целом превышают плюсы. Имеет смысл использовать их только как подмену шлюзов во внешние API, ну и иногда служб из легаси-кода, которые очень трудно протестировать.
  • Если решили писать код без теста, желательно при его создании думать «а что, если в будущем мы все-таки захотим написать на это тест?»
  • Тесты должно быть писать легко и приятно, они придают надежности, уверенности, помогают лучше понимать код, управлять зависимостям.
  • Обращаем внимание на читабельность тестов. Надо относиться к коду теста так же, как и к коду, который он покрывает.
  • Фикстуры БД – часть теста, тоже должны быть читабельными
Теги
Показать больше

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

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

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

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