В Островке есть два основных продукта: для пользователей (ostrovok.ru) и «админка» для отелей (экстранет), куда подписанные нами отели заносят данные. Это отдельные продукты, со своими командами и различным отношением к разработке через тестирование (TDD). Одинаковая платформа: django и postgres. В экстранете используют TDD и у них куча тестов. Поначалу тесты были и в ostrovok.ru, но ввиду ухода части адептов в экстранет и очень интенсивного развития их перестали поддерживать. В общем передо мной встала задача внедрить тестирование. Первые шаги сделаны и хочу поделиться этим опытом и решениями, которые были применены.
У нас есть отдел QA и Selenium автотесты, но это отдельно.
С django и тестами вообще дела обстоят довольно хорошо и конечно лучше с самого начала все покрывать тестами, наращивая функционал и делая рефакторинги.
В нашем случае уже существовал огромный функционал и очень много всесторонних зависимостей и интеграции с внешними API. И нужно, чтоб это все работало в тестовой среде. Про быстрый SQLite в памяти можно забыть, в проекте есть привязки к особенностям postgres, да и идентичность тестового окружения все таки важна, поэтому тесты тоже работают на postgres.
Какие тесты мне нравятся и почему TDD
Существует много видов тестирования, которые различаются по разным аспектам.
По изоляции мне нравятся больше интеграционные тесты, по тестируемому объекту — функциональные.
У таких тестов очень большое покрытие кода, это и плюс и минус одновременно.
Минус:
- найти сломанное место иногда трудно
Плюсы:
- python интерпретируемый язык, ошибки могут вылезти в момент исполнения кода, а если он покрыт тестами, то можно уверено сказать, что этот код не падает;
- такие тесты высокоуровневые и нам зачастую не страшны детали реализации, т.е. нужно меньше их править, хотя править конечно же приходится.
Мы разрабатываем веб и в идеале мне не хочется открывать браузер для ручного тестирования моего кода. Хочется записать в тест все действия в браузере и добавить ряд проверок (отправка письма, наличие лога или какого-то объекта в базе). Когда буду писать код, мне нужно провести все эти действия вручную один раз точно, но в большинстве случаев это будет несколько раз. Записать действия в тест и прогнать десять раз по несколько секунд это намного круче, чем вручную сделать десять проверок. В браузере кроме основной разметки еще подгружаются стили, картинки, javascript и все это обычно сваливается на наш локальный runserver, а он не самый шустрый и зачастую работает в одном потоке, т.к. настраивать для разработки связку uwsgi и nginx как-то не хочется… Ну и в добавок выгода в том, что написанный тест, который помог в разработке, остается и играет важную роль в регрессионном тестировании.
Кроме тестирования http запросов есть и другие тесты, например, тестирование django команд, с ними все аналогично. Обычные юниттесты тоже полезны. Когда привыкаешь пускать и писать тесты, то и стиль разработки меняется, процесс скорее будет итеративным: простой тест — нужный код, усложняем тест — дописываем код. Например: можно сделать опечатку, быстро запустить тест и увидеть опечатку и что тест не прошел.
Да и конечно есть места, где ручное тестирование сложно или даже почти невозможно, в этом случае тесты — это необходимость. Например: проверка правильного перехвата исключений и обработки ошибок, тонкие места логики…
В идеале — сначала тест.
Расписывать все преимущества разработки через тестирование не цель данной статьи, оставим это другим, например, Кенту Беку.
Как сделать запуск тестов быстрее?
В TDD очень важная операция — запуск тестов. Обычно это даже не все тесты, а какой-то набор: тесты из пакета, отдельного модуля или вообще отдельный тест. Поэтому запуск отдельных тест кейсов должен быть быстрым.
В django с этим проблема, в ней перед каждым запуском теста создается база, и если схема большая, то это может занять и 30 секунд, а выполнение конкретного теста — меньше секунды. Не хочу ждать пока создается база.
Решение: вынести создание базы в отдельный шаг (использовать базу из предыдущих прогонов).
В рамках наших условий кроме схемы базы нам еще понадобились начальные данные:
- с отдельного внутреннего ГИС сервиса, сам сервис живет своей жизнью, предоставляя REST интерфейс;
- в http тестах часто нужны загруженные отели.
Кажется что тут нового:
- в django есть фикстуры, правда они статические и их не очень приятно поддерживать — поэтому нет;
- есть ряд библиотек для генерации динамических фикстур: раз, два, три. Они имеют право на жизнь, но у нас отель — довольно сложная сущность, поэтому генерация автоматически — нет.
Используй существующий код!
В «безтестное» время мне пришлось поучаствовать в мега рефакторинге, который был связан с импортом отелей. В ходе этой задачи у нас появились тесты хорошо покрывающие импорты. Эти тесты жили своей жизнью, мы их поддерживали в актуальном состоянии, чтоб они не стали мертвым грузом как другие существующие тесты, большую часть которых удалили.
Еще раз повторюсь, отели — сущность сложная, и создавать все связные объекты, а потом поддерживать все это хозяйство, совсем не хотелось. Тем более есть рабочий, протестированный код импортов, задача которых как раз создавать отели, его и заюзали. Меньше кода — меньше ошибок.
Тесты мы гоняем с nose, в целом это очень хороший инструмент для запуска тестов с поддержкой плагинов.
В итоге у нас получились свой раннер и ряд плагинов, решающие несколько проблем:
- независимый шаг создания базы;
- обвертка для сброса базы (транзакции или уникальная база на тест);
- слежение за состоянием базы после теста в режиме транзакций;
- изолированность от внешнего мира (внешние http запросы должны мокаться);
Есть процесс создания базы в зависимости от параметров командной строки:
...
--with-reuse-db # включает реиспользование базы, можно включить в настройках
--create-db # при включенном первом флаге пересоздает базу
...
В этом подходе есть минус: нужно помнить, что если меняется схема базы, то нужно базу пересоздать. Это не критично, важнее быстрый запуск.
Процесс создания начальной базы у нас уже может занимать до минуты при импорте ГИСа и отелей. Причем мы сохраняем две начальные базы: с отелями и без, т.к. при тестировании импортов нам отели не нужны. В конкретных TestCase
мы задаем нужный нам шаблон базы.
В стандартном django подходе из TransactionTestCase делается flush
(полная очистка базы), потом восстанавливается начальная. Этот подход не работает, т.к. у нас отдельный шаг по созданию базы и чистить ее не нужно. При опции autocommit для postgres, flush
выполнялся на каждый тест и это плохо — он долгий.
Чтоб ускорить тесты (относительно flush
) мы использовали уникальную базу, которая создавалась по шаблону, postgres такое умеет:
src = self.db_conf['TEST_NAME']
new = '{0}_{1}'.format(src, uuid.uuid4().hex)
psql(
'DROP DATABASE IF EXISTS "{0}";'
'CREATE DATABASE "{0}" WITH TEMPLATE "{1}";'
.format(new, src)
)
Прирост был относительно flush
в несколько раз и это казалось уже неплохо. Плюс уникальной базы на тест в том, что вероятность каких-то коллизий в базе нулевая, а с транзакциями они возможны. В конце концов пришли к варианту: по умолчанию работа в транзакции, т.к. это быстрее, а если у каких-то тестов проблемы — то уникальная база.
Для ускорения тестовой базы можно еще поставить в postgresql.conf:
fsync = off # turns forced synchronization on or off
Прирост тоже ощущается. Ну и SSD винчестеры тоже хорошо :).
Такие тесты проще включить в процесс сборки, они достаточно быстро проходят (3-4 минуты ~250 тестов) и не задерживают особо релиз, они рядом с кодом. За временем выполнения тестов нужно следить и принимать меры по ускорению, т.к. количество тестов будет только расти, а значит — и время их выполнения.
Дальше в плане ускорения нужно параллелить запуск тестов, nose даже умеет, но свой код нужно дорабатывать.
Кроме быстрого запуска тестов, нужно еще и их ненапряжное написание. Когда у нас куча всесторонних зависимостей, первые тесты, которые повторяют основные действия пользователя, даются тяжело. Много мест нужно замокать, с многими местами разобраться. Поэтому было выделено время, чтоб сделать помощники, упрощающие написание таких тестов, с минимумом кода.
Что мы имеем?
Благодаря существенному ускорению запуска тестов теперь они участвуют в сборке пакета: релиз не выкатывается, если есть упавшие тесты. Это тоже очень важный момент, т.к. есть явная связь: работающие тесты — релиз, неработающие тесты — нет релиза (релизы у нас частые, бывают несколько раз в день). Selenium автотесты живут пока отдельной жизнью, но команда работает над включением их в процесс непрерывной интеграции.
Тесты нам уже помогают:
- ловят некоторые баги, на этапе сборки релиза;
- переезд с django 1.3 на 1.4, частично заслуга тестов;
- некоторую логику вручную проверить сложно, а в тестах нет (касается наших импортов отелей);
- с тестами стало немного уверенней.
В принципе начало положено, решения приняты, что будет дальше — время покажет.
P.S. python и postgres отличные инструменты — используйте.
Автор: naspeh
Автор: Ostrovok