Note
Статья была написана для habrahabr.
В Островке есть два основных продукта: для пользователей (ostrovok.ru) и “админка” для отелей (экстранет), куда подписанные нами отели заносят данные. Это отдельные продукты, со своими командами и различным отношением к разработке через тестирование (TDD). Одинаковая платформа: django и postgres. В экстранете используют TDD и у них куча тестов. Поначалу тесты были и в ostrovok.ru, но ввиду ухода части адептов в экстранет и очень интенсивного развития их перестали поддерживать. В общем передо мной встала задача внедрить тестирование. Первые шаги сделаны и хочу поделиться этим опытом и решениями, которые были применены.
Note
У нас есть отдел QA и Selenium автотесты, но это отдельно.
С django и тестами вообще дела обстоят довольно хорошо и конечно лучше с самого начала все покрывать тестами, наращивая функционал и делая рефакторинги.
В нашем случае уже существовал огромный функционал и очень много всесторонних зависимостей и интеграции с внешними API. И нужно, чтоб это все работало в тестовой среде. Про быстрый SQLite в памяти можно забыть, в проекте есть привязки к особенностям postgres, да и идентичность тестового окружения все таки важна, поэтому тесты тоже работают на postgres.
Существует много видов тестирования, которые различаются по разным аспектам.
По изоляции мне нравятся больше интеграционные тесты, по тестируемому объекту - функциональные.
У таких тестов очень большое покрытие кода, это и плюс и минус одновременно.
Мы разрабатываем веб и в идеале мне не хочется открывать браузер для ручного тестирования моего кода. Хочется записать в тест все действия в браузере и добавить ряд проверок (отправка письма, наличие лога или какого-то объекта в базе). Когда буду писать код, мне нужно провести все эти действия вручную один раз точно, но в большинстве случаев это будет несколько раз. Записать действия в тест и прогнать десять раз по несколько секунд это намного круче, чем вручную сделать десять проверок. В браузере кроме основной разметки еще подгружаются стили, картинки, javascript и все это обычно сваливается на наш локальный runserver, а он не самый шустрый и зачастую работает в одном потоке, т.к. настраивать для разработки связку uwsgi и nginx как-то не хочется… Ну и в добавок выгода в том, что написанный тест, который помог в разработке, остается и играет важную роль в регрессионном тестировании.
Кроме тестирования http запросов есть и другие тесты, например, тестирование django команд, с ними все аналогично. Обычные юниттесты тоже полезны. Когда привыкаешь пускать и писать тесты, то и стиль разработки меняется, процесс скорее будет итеративным: простой тест - нужный код, усложняем тест - дописываем код. Например: можно сделать опечатку, быстро запустить тест и увидеть опечатку и что тест не прошел :).
Да и конечно есть места, где ручное тестирование сложно или даже почти невозможно, в этом случае тесты - это необходимость. Например: проверка правильного перехвата исключений и обработки ошибок, тонкие места логики…
В идеале - сначала тест.
Расписывать все преимущества разработки через тестирование не цель данной статьи, оставим это другим, например, Кенту Беку.
В TDD очень важная операция - запуск тестов. Обычно это даже не все тесты, а какой-то набор: тесты из пакета, отдельного модуля или вообще отдельный тест. Поэтому запуск отдельных тест кейсов должен быть быстрым.
В django с этим проблема, в ней перед каждым запуском теста создается база, и если схема большая, то это может занять и 30 секунд, а выполнение конкретного теста - меньше секунды. Не хочу ждать пока создается база.
Решение: вынести создание базы в отдельный шаг (использовать базу из предыдущих прогонов).
Используй существующий код!
В “безтестное” время мне пришлось поучаствовать в мега рефакторинге, который был связан с импортом отелей. В ходе этой задачи у нас появились тесты хорошо покрывающие импорты. Эти тесты жили своей жизнью, мы их поддерживали в актуальном состоянии, чтоб они не стали мертвым грузом как другие существующие тесты, большую часть которых удалили.
Еще раз повторюсь, отели - сущность сложная, и создавать все связные объекты, а потом поддерживать все это хозяйство, совсем не хотелось. Тем более есть рабочий, протестированный код импортов, задача которых как раз создавать отели, его и заюзали. Меньше кода - меньше ошибок.
Тесты мы гоняем с nose, в целом это очень хороший инструмент для запуска тестов с поддержкой плагинов.
Есть процесс создания базы в зависимости от параметров командной строки:
$ ./manage.pt test --help ... --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 в несколько раз и это казалось уже неплохо. Плюс уникальной базы на тест в том, что вероятность каких-то коллизий в базе нулевая, а с транзакциями они возможны. В конце концов пришли к варианту: по умолчанию работа в транзакции, т.к. это быстрее, а если у каких-то тестов проблемы - то уникальная база.
Note
Для ускорения тестовой базы можно еще поставить в postgresql.conf:
fsync = off # turns forced synchronization on or off
Прирост тоже ощущается. Ну и SSD винчестеры тоже хорошо :).
Такие тесты проще включить в процесс сборки, они достаточно быстро проходят (3-4 минуты ~250 тестов) и не задерживают особо релиз, они рядом с кодом. За временем выполнения тестов нужно следить и принимать меры по ускорению, т.к. количество тестов будет только расти, а значит - и время их выполнения.
Дальше в плане ускорения нужно параллелить запуск тестов, nose даже умеет, но свой код нужно дорабатывать.
Кроме быстрого запуска тестов, нужно еще и их ненапряжное написание. Когда у нас куча всесторонних зависимостей, первые тесты, которые повторяют основные действия пользователя, даются тяжело. Много мест нужно замокать, с многими местами разобраться. Поэтому было выделено время, чтоб сделать помощники, упрощающие написание таких тестов, с минимумом кода.
Благодаря существенному ускорению запуска тестов теперь они участвуют в сборке пакета: релиз не выкатывается, если есть упавшие тесты. Это тоже очень важный момент, т.к. есть явная связь: работающие тесты - релиз, неработающие тесты - нет релиза (релизы у нас частые, бывают несколько раз в день). Selenium автотесты живут пока отдельной жизнью, но команда работает над включением их в процесс непрерывной интеграции.
В принципе начало положено, решения приняты, что будет дальше - время покажет.
P.S. python и postgres отличные инструменты - используйте.