# тесты - расшифр
## Автотесты
Было около 2500 автотестов за 3 года работы. Почитали книжек, начали разбираться, почему много багов, ошибок, сложно рефакторить, открыли для себя автотесты.
Термины
Модульный тест (юнит тест) - тест метода ***где же версии???***
Функциональный тест - тест контроллера или компонента, если речь о фронтенде
Приемочный тест - end-to-end, браузер или эмулятор браузера исполняет сценарий
Фикстура - состояние тестового окружения, необходимое для работы теста (глобальны епеременные, данные в БД и рроч необходимое для сценария теста)
***ничего не понимаю!!!***
> [коммент]Картинка - классические слои системы, пользовательскимй интерфес, база данных, юнит тест внутри польтзовательского теста покрывает малую часть. Функциональный тест берет больше - покрывает несколько слоев, выбирает срез функциональности и ее тестирует. НЕ включает пользовательский интерфейс, но Может кстати включать базу данных, можно было побольше нарисовать. Приемочный тест - покрывает все слои системы.
Приемочные тесты:
Плюсы - очевидны из названия, покрывают всю систему сверху донизу, позволяет убедиться, что все работает как следует, можем принять программу и запустить ее.
Недостатки: обратная связь от них очень медленная, долго срабатывают, не очень надежны, ложные срабатывания, что-то отваливается, по этой причине... когда начинали писать, драйвера не могли обнаруживать некоторые элементы, которые мы видели глазами (сейчас наверно исправлено), поэтому мы вообще отказались от приемочных тестов на пред работе.
Модульные
Плюсы: срабатывают быстро, их просто писать. Большая фикстура не нужна, покрывают небольшой кусок кода, не нужно много состояний.
МИнусы: неустойчивы к изменениям структуры кода: если нужно слить два метода в один или разделить, выделить класс, удалить метод, постоянно приходилось переписывать тексты. Неявный подводный камень: неустойчивы к рефакторингу, когда изменяется архитектура или внутренняя структура кода.
Промежуточный вариант - функциональные тесты.
Плюсы: надежнее приемочных
более устойчивы к изменениям структуры кода, чем модульные
Минусы: медленнее модульных
сложнее в написании, т.к. надо подготовить большую фикстуру
Мы начали писать тесты, писали много модульных и функциональных, первый вызов - скорость срабатывания тестов. Тестов стало много, надо было долго ждать, даже когда локально запускали на комп разработчиков. Есть такой подход - разработка через тестирование, он предполагает запуск модульных тестов неск раз в день, после каждого изменения, при низкой скорости срабатывания это невозможно. Узкое место - работа с базой данных.
Стали думать, как побороть это. Первый опыт - опыт с моками.
*Мок в PhpUnit - объект, создается динамически, класс которого создается динамически наследованием от пародируемого класса. Мы можем настраивать, что будут возвращать методы мока, можем проверять, какие методы мока сколько раз с какими параметрами были вызваны*
мок создается динамически, по умолчанию его методы ничего не выполняют, но он наследуется от класса, методы можно подменять как надо, главное потом проверить, какие были вызваны.
Моки позволяют отрезать целые куски функциональности. Подменяя службу моком, мы избавляемся от необходимости думать, что там происходит, разрабатывать дополнительные сценарии, писать фикстуры, чтобы это заработало. Позволяет писать меньше фикстур, и скорость срабатывания выше, потому что мы можем отрезать целые блоки кода, которые выполняют какие-то запросы у БД.
И еще неявный плюс - заставляют лучше организовывать зависимости, потому что когда ты пишешь код и понимаешь, что придется на него написать тест, и что-то придется подменять моками, задумываешься о зависимостях, иначе сложно будет подменить моком то, что надо.
Минусы: слишком привязываем код теста к реализации, потому что мы должны в ходе теста создать мок-объект, подумать, какие должны быть у него вызваны методы, большая привязка к реализации.
Также обнаружили, что тесты стали менее надежны (не чувсвтвительны даже к изменению интерфейса, не говоря о реализации). Т.е. мы могли даже удалить метод, и через какое-то время обнаруживали, что тесты, покрывающие этот метод, работали, потому что оставался его мок. Мок по-прежнему делал вид, что все хорошо.
Опыт с моками считаю неудачным в плане ускорения.
Следующий вариант для ускорения - СУДБ SQLite. Умеет создавать БД в памяти. Основная наша БД была PostgreSQL, написали транслятор схемы в SQLite (там она гораздо проще, меньше возможностей). После каждой миграции делали запросы, которые получали метаданные, и написали обработчик, который генерировал запросы уже в формате SQLite. Благодаря этому скорость на локальных машинах выросла раза в 2-4. Стало реально прогонять весь комплект тестов несколько раз в день.
Минусы - потеряли многие нативные возможности PostreSQL (json, некоторые удобные агрегатные функции и проч.). Запросы пришлось писать так, чтобы они срабатывали и на Постгрес, и на Склайт, а Склайт беднее. У нас был еще стейджинг-сервер, где мы использовали PosgreЫЙД, чтобы приблизить максимально к боевому, ждали билда минут по 30.
Долго так мучились, потом узнали, что postgres можно оптимизировать для автотестов. Эта оптимизация помогла нам сократить время срабатывания примерно в четыре раза. Для это надо добавить неск настроект в postgresql.conf:
```
fsync=off
synchronous_commit=off
full_page_writes=off
```
Это дюрабилити (fsync?), нужна, чтобы сервер гарантировал, что даже если в середине транзакции умрет, транзакция все равно завершится, когда стартует.
Понятно, что на продакшне такие настройки делать нельзя, но на автотестах ок.
Такая настройка применяется для всего кластера, затронет все БД, нельзя сделать для какой-то одной. Если получается локализовать базы в отдельный кластер и отключить fsync - прекрасно.
Так поборолись со скоростью срабатывания тремя шагами.
Второй вызов, который обнаружили со временем - сложность создания фикстур БД. Вот так выглядел кусок кода, создававший фикстуру, имена колонок выносили в константы, это просто заполнение таблиц баз данных с помощью массива. Стоит сказать, что так оно и осталось, т.е. по-прежнему проблема, но она не так сильно беспокоила.
Вывыод:
- отказались от приемочных тестов, хотя сейчас я бы снова попробовал, скорее всего драйвера пофиксили многие баги.
- если нужно покрыть новый функционал тестами, мы будем писать только тесты контроллеров/компонентов, потому что большой риск структурных изменений, придется рефакторить и сильно менять структуру кода.
- стараемся писать таких тестов не много (много = медленно, срабатывают не так быстро, как модульные). Покрывать только те случаи, которые могут выстрелить, имеют вероятность ошибки в будущем.
- модульные тесты пишем на алгоритмически-насыщенные методы (сложная логика, которую надо тестить) или на методы с небольшим риском структурного изменения в будущем.
- от моков сильно отказались из-за минусов, моками стараемся подменять только шлюзы во внешние API. Может если тестируем легаси-код, отрезаем службу, которую очень сложно протестировать.
- взяли себе за правило при написании кода (даже если не собираемся покрывать тестом) всегда думать "а что если в будущем мы захотим написать на это тест?"
(за три года работы выводы)
немного о `new`
еще один вывод, который сделали, это опасность оператора `new`. Службы, созданные с его помощью, невозможно подменить моками и стабами (mock и stub). Вывод:
- не использовать `new` для создания объектов, которые по своей сути являются службами; использовать только для создания объектов, являющихся моделями или value-objectами или сущностями.
- можно использовать в фабриках, поому что сами фабрики можно будет подменить, но сами фабрики не должны создаваться через `new`.
- можно использовать для создания моделей, entities, DTO (data transfer object), value-objects
Одна из вещей - это создание даты и времени через `new` (`new DateTime`).
При создании лбъекта даты и времени без параметров произойдет обращение к службе системного времени - это внешняя служба по отношению к приложению.
Вот вариант кода, как может выглядеть создание даты. Хотим закрыть домашнюю работу, позже в тесте хотим проверить, что получили домашнюю работу, проверить, что она равна текущему времени. Будет проблема, может пройти пара мс, и текущее время не будет равно. Должна быть возможность управлять тем, что получаем в пустых скобках, где обращение к системному времени.
Один из вариантов решения - простенький timeService на 20 строчек, который просто оборачивает создание текущих дат, который можно подменить стабом, поставить туда нужную дату, и в этом месте этот стаб вернет нужное время, легко протестируем.
Второй вариант - использовать библиотеку Carbon, мне этот вариант не очень нравится. Ее суть в том, она наследуется от объектов dateTime, представляет ряд наследованных объектов, и статические методы, которые оборачивают. Выглядит так же - нужно где-то вызвать Карбон, статический метод now, он создаст текущее время, и потом предоставляет интерфейс для подмены этого текущего времени.
Предложения для Vimbox:
Фикстура. Проблема в тестах в том, что она громоздка, занимает много строк, ее сложно читать. Фикстуры БД. Тем не менее, они должны быть частью теста. Вариант использовать regions и настройки по умолчанию iDE скрывать длинные фикстуры в наших тестах.
Элементы фикстуры, относящиеся к тесту, должны быть расположены в том же файле (классе?) теста, где и сами тесты, а не во внешнем файле (не в классе фикстуры, например). Фикстура - неотъемлимая часть теста, она задает начальные условия, с ее помощью можем регулировать выход теста, что получим в результате и что будем проверять.
Вот один из вариантов, как можно реализовать. Подключем Fixture Bundle, он позволяет через `addFixture` создавать классы фикстур, мне нравится подход с моделями `builderModel` (?), которые позволяют наполнить БД нужным содержимым.
Фикстура должна быть читабельной - когда читаешь тест, а тест должен быть с документацией к коду, чтение фикстуры - часть самого теста, приходится трудиться, чтобы ее прочитать, и подход с `builderModel' как раз позволяет это делать, это такие объекты значения, куда мы устанавливаем нужные нам поля баз данных, специальный объект выполняет фикстуру и заполняет базу.
Заполняем только самое необходимое, чтобы модели были как можно короче, чтобы сократить код самой текстуры (класс фикстуры должен предоставлять набор умолчаний)
Привлекать внимание к важным частям фикстуры с помощью комментариев. В этом случае я комментом указал, что вот здесь первый студент - это тот, на которого в тестируемом коде будет установлен фильтр, а вот здесь я устанавливаю студента с id 2, эта строка не должна будет войти в результат моего теста и привлекаю внимание комментарием, без комментария эту двойку легко не заметить.
Делаем ставки: тест - это ставка своего времени, мы можем написать тест на код,к оторый никогда не будет сломан, и тогда время будет потрачено впустую. чтобы этого не происходило, нужно искать места в тестируемом коде, особенно если не разработка через тестирование, а пишем тест после того, как написали код. Можно искать места, которые могут выстрелить. Вот пример: есть какой-то запрос, чей результат будет тестироваться, и вот это условие, если кто-то будет этот код рефакторить, может легко быть забыто. Я добавляю соответствующую запись в фикстуру БД, помечаю ее комментарием, добавляю проверку, что не попадет в результат вот эта запись. Т.е. стараемся тестировать не все подряд, а только то, что может выстрелить.
Резюме
- тесты должно быть писать легко и приятно, тогда программисты будут их писать, они придают надежности, уверенности, помогают лучше понимать код, управлять зависимостям.
- акцент на тестах контроллеров (пишем и юнит-тесты (методов), но помним, что они уязвимы к структурным изменениям).
- не злоупотребляем моками (желательно заменять только внешние API либо какое-то легаси).
- держим в голове "а что если в будущем захотим написать на это тест?"
- оптимизировать postgreSQL на тестингах. Пока 100 тестов это ок, но Когда количество тестов приблизиться к тысяче, начнем замечать замедление тестов, тогда надо оптимизировать
- обращаем внимание на читабельность тестов. Надо относиться к коду теста так же, как и к коду, который он покрывает, потому что это как документация. Разработчику будет удобно сперва прочитать код теста, а потом уже код, который он покрывает.
- фикстуры БД - часть теста, тоже должны быть читабельными
- помним, что тест - это ставка нашего времени, которое может быть потрачено в пустую.
- тесты писать надо!
Проблему решали: от этого не завсиела жизнь, но было неприятно много багов в продакшне, необходимость тестить их вручную, сложность что-то с этим поделать, сложность читать код и его рефакторить, тесты помогают изменять код, делают его более гибким, придают уверенности.
Насколько замедляет или ускоряет разработку?
Разработка тестов замедляет выполнение задачи, в книжках пишут что в долгосрочной перспективе это должно окупиться, но померять это оч сложно.
Девочки любят на 100% покрывать тестами. Такой код очень сложно сломать. Новичкам легко работать в такой среде, потому что сложно допуатить ошибку, если что-то не так, тест завалит сразу. У нас такой фанатизм не нужен, но минимум быть должен.
Оптимизация постгрес: что может быть страшного: ты выполняешь транзакцию, сервер умер, получишь нецелостное состояние базы или вообще нечитаемую, можно перезалить. Прирост производительности приятнее, чем этот риск, тем более что сервер падает нечасто.
коммент:
еще минус функциональных тестов - точность определения проблемы, когда тест падает. Если браузерный тест, ошибка выглядит как "кнопочка нажата, элемент не появился" - не понятно, что вообще. А когда падает юнит-тест, он тебе говорит, что не можешь вызвать метод, он undefined. чем более низкоуровневый тест, тем точнее он показывает проблему.
когда все что нужно покрыто тестами, повышает скорость команды, потому что она увереннее, и напрямуюю влиает на time to market. Это низкоуровневые тесты.