Быстрые тесты в Django

Медленные тесты не только тратят время разработчиков на ожидание, но и усложняют следование лучших практик TDD (red-green testing). Когда тестовый набор выполняется несколько минут или дольше - это приводит к тому, что весь набор тестов запускают редко и баги, которые можно было бы исправить раньше и быстрее, откладываются.

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

Параллельные тесты

Самый простой способ ускорить выполнение тестов без внесения каких-либо изменений в код - запуск тестов параллельно. Для этого в Django нужно задать параметр --parallel при запуске тестов, также этот параметр принимает опционально количество процессов. Если количество процессов не задано - берется равное количеству ядер процессора, для большинства случаев это оптимально.

Последовательное выполнение тестов из примера на моей машине длится:

# python manage.py test
...........
----------------------------------------------------------------------
Ran 11 tests in 8.012s

Если запустить с параметром --parallel:

# python manage.py test --parallel
...........
----------------------------------------------------------------------
Ran 11 tests in 2.628s

Тесты выполнились больше чем в 3 раза быстрее.

Стоит отметить, что Django распределяет выполнение различных тест кейсов (подклассов unittest.TestCase) между разными процессами. Следовательно, если у вас тест кейсов меньше, чем количество ядер у процессора, то Django уменьшить количество процессов до количества тест кейсов. В нашем примере только 3 тест кейса, что ограничивает параллелизм 3мя процессами. На реальных проектах у вас как правило будут сотни или даже тысячи тест кейсов и эта проблема не будет актуальной.

Также при запуске тестов параллельно, Django иногда не может собрать трейсбеки ошибок, в случае ошибки вам придется перезапустить весь набор тестов последовательно.

Использование слабого алгоритма хэширования паролей

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

Для тестовых целей нам не нужен такой сложный алгоритм хэширования, мы можем использовать что-то быстрое, например, MD5. Добавим в settings.py переключение на MD5 при запуске приложения в режиме тестирования:

import sys

TESTING = 'test' in sys.argv

if TESTING:
    PASSWORD_HASHERS = [
        'django.contrib.auth.hashers.MD5PasswordHasher',
    ]

Протестируем время выполнения тестов после этого изменения:

# python manage.py test --parallel
...........
----------------------------------------------------------------------
Ran 11 tests in 0.564s

Быстрее в 4.65x раз 🚀 Я видел, как этот простой хак увеличивал скорость выполнения огромного набора теста на порядок.

Создаем данные, когда они нужны

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

Для решения этой проблемы нужно по максимуму упростить общий setUp метод, в идеале лучше вообще удалить его. Создание тестовых данных же вынести в конкретные тест кейсы и создавать только те данные, которые действительно нужны.

Я внес соответствующие изменения в наш пример. Посмотрим как это отразится на времени выполнения тестов:

# python manage.py test --parallel
...........
----------------------------------------------------------------------
Ran 11 tests in 0.353s

Еще на 60% быстрее.

setUpTestData

Базовый тест кейс Django предоставляет возможность создавать тестовые данные на уровне тест кейса, а не каждого теста. Это позволяет значительно ускорить выполнение тестов. Для этого нужно вынести создание данных в метод класса setUpTestData.

Созданные в setUpTestData объекты не должы меняться в процессе тестирования иначе это может привести к не стабильным тестам, т.к. тесты не будут полностью изолированными.

Здесь я добавил изменения в пример. Посмотрим на время выполнения тестов:

# python manage.py test --parallel
...........
----------------------------------------------------------------------
Ran 11 tests in 0.349s

Существенного преимущества мы не получили, но давайте проверим, что будет, если добавить еще несколько тестов. Без setUpTestData я получил результат 0.563s, с setUpTestData - 0.348s. Т.е. при использовании setUpTestData при добавлении новых тестов время практически не растет (кроме времени выполнения самого теста), т.к. не нужно для каждого нового теста заново создавать данные.

Заключение

Желательно с самого начала разработки обращать внимание на скорость выполнения тестов. Используя ряд не сложных методов вы можете добиться очень быстрого выполнения тестов и получать максимальную пользу от автоматического тестирования.

live long and prosper 🖖