Які можливості тестування з'явились у Django 4.0

9 хв. читання

Наприкінці вересня з'явився перший альфа-реліз Django 4.0, а остаточний випуск запланований на грудень. Ми не будемо зупинятись на усіх нових можливостях Django, їх безліч, детально усі вони описані у примітках до випуску. А ми ж сьогодні заглибимося у ті зміни, що стосуються тестування.

1. Довільний порядок тестування за допомогою --shuffle

У примітках до випуску вказано:

Тести Django тепер підтримують опцію --shuffle для виконання тестів у випадковій послідовності.

Чудово, що у Django з'явилася така можливість. Це дає нам надійний захист від неізольованих тестів. Коли тести не ізольовані, один тест залежить від виконання іншого. Наприклад, візьмемо цей:

from django.test import SimpleTestCase

from example.core.models import Book


class BookTests(SimpleTestCase):
    def test_short_title(self):
        Book.SHORT_TITLE_LIMIT = 10
        book = Book(title="A Christmas Carol")

        self.assertEqual(book.short_title, "A Chris...")

    def test_to_api_data(self):
        book = Book(title="A Song of Ice and Fire")

        self.assertEqual(
            book.to_api_data(),
            {"title": "A Song of Ice and Fire", "short_title": "A Song ..."},
        )

Тести виконуються під час прямого запуску (спочатку test_short_title), але не працюватимуть у зворотному порядку. Причина у тому, що test_short_title виконує мавполатування (monkeypatch) Book.SHORT_TITLE_LIMIT до нового значення, а очікувані дані test_to_api_data залежать від цієї зміни.

Неізольовані тести надто легко можуть впливати на змінювану кодову базу. А оскільки вони є «дистанційними діями», їх неможливо виявити під час перегляду коду. Через це ми будемо шукати проблеми з ізолюванням та гаяти час.

На щастя, можна захиститися від збоїв ізолювання, якщо запустити наші тести у кількох різних порядках. Є дві прості техніки: час від часу змінювати порядок (наприклад, у CI), або змішувати його щоразу.

Зворотний порядок ефективний, однак він не може виявити кожен збій ізоляції.

Якщо ж перемішувати щоразу, зрештою ми перевіримо кожну доступну почерговість. За кілька таких спроб зазвичай виявляються всі помилки ізолювання. Щоб дозволити повторити невдалий порядок тестів, ми використовуємо псевдовипадковість на основі Python-модуля random.

Ізолювання тестів розглянуто у книзі «Speed Up Your Django Tests». У ній розповідається, як використовувати ці техніки у двох популярних фреймворках тестування. До четвертої версії Django підтримував лише гіршу техніку зворотного порядку:

Тестований фреймворк Опція зворотного порядку Опція довільного порядку
Django --reverse flag --shuffle flag починаючи з Django 4.0 🎉
pytest pytest-reverse --reverse flag pytest-randomly

Радимо завжди використовувати випадковий порядок тестів. Ми можемо зробити його типовим у Django 4.0+ за допомогою власного налаштування TEST_RUNNER:

(Для pytest просто встановіть pytest-randomly.)

2. --buffer із --parallel

У примітках до випуску про цю зміну читаємо:

Django Test Runner тепер підтримує опцію --buffer з паралельними тестами.

Опція --buffer успадкована Django від модульного тестування. Коли активний модульний тест отримує вивід за кожного тестування, він показує лише провалені тести. Це те саме, що типово робить pytest.

Теоретично, тести повинні бути добре написані, щоб отримати вивід з усіх викликаних функцій. На практиці це зробити важко, оскільки ви по-різному додаєте вивід та журналювання. Через деякий час ваші тести можуть вже не показувати акуратний ряд крапок, а натомість повертатимуть купу сторінок невідповідного тексту.

Через велику кількість результатів тести можуть значно сповільнитися: ваша термінальна програма повинна зберігати, відтворювати та гортати весь цей текст. Це запросто може сповільнити виконання втричі.

Розробник Баптист Міспелон (Baptiste Mispelon) допоміг Django з додаванням підтримки --buffer у версії 3.1. Але через обмеження структури тестів Django опція не підтримувалася при використанні --parallel. Тож її користь трохи зменшилась під час запуску повного набору тестів, оскільки прискорення тестування з --parallel більше, ніж сповільнення через відсутність --buffer.

Починаючи з Django 4.0, завдяки деякій внутрішній перебудові, можна використовувати --buffer разом з --parallel.

Так ми можемо увімкнути --buffer одразу з коробки, створивши власний клас для тестів:

from django.test.runner import DiscoverRunner


class ExampleTestRunner(DiscoverRunner):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.buffer = True

3. --parallel auto

Ось примітка до випуску:

Опція test --parallel відтепер підтримує значення auto для запуску одного процесу тестування для кожного ядра процесора.

Це проста зміна до цієї корисної опції.

Раніше ви могли вказати --parallel запускати кількість тестових процесів, яка збігається з кількістю ядер процесора, або --parallel N для N процесів. Якщо ви запустили тест типу manage.py test --parallel example.tests, то example.tests інтерпретується як хибна специфікація для N процесів. Можна було використати -- spacer (наприклад, manage.py test --parallel -- example.tests), але це неочевидна функція argparse.

Починаючи з Django 4.0, можна вказати --parallel auto, тож будь-які подальші аргументи будуть правильно інтерпретовані. Ця зміна корисна як для безпосередньо запущеного тесту, так і для скриптів обгортання.

4. Автовимкнення серіалізації бази даних

Про цю зміну маємо дві примітки. Перша трохи загадкова та охоплює внутрішні зміни:

Новий аргумент serialized_aliases для django.test.utils.setup_databases визначає, для яких псевдонімів (aliases) DATABASES тестових баз даних слід серіалізувати стан, щоб дозволити функціональність serialized_rollback.

Друга стосується припинення підтримки налаштувань баз даних, які більше не потрібні:

Налаштування тестів SERIALIZE більше не підтримується, оскільки його можна замінити за допомогою TestCase.databases з увімкненою опцією serialized_rollback.

Розшифруймо.

Зазвичай, коли TransactionTestCase відкочує базу даних, усі таблиці залишаться порожніми. Це означає, що будь-які дані, створені під час міграції, будуть відсутні під час таких тестів.

(Таке відкочування може також статися у TestCase при використанні нетранзакційної бази даних, зокрема MySQL з MyISAM.)

Щоб виправити цю проблему, TestCaseтаTransactionTestCase мають прапор serialized_rollback. Він змушує перезавантажити вміст відкоченої бази даних після застосування команди FLUSH до таблиць.

Аби підтримати це відкочування, Django завжди серіалізуватиме всю базу даних на початку запуску тестів. Це вимагає часу та пам'яті, адже Django зберігає дані у (потенційно великому) рядку в запам'ятовування.

Більшість проєктів не використовує serialized_rollback, бо ця попередня робота над серіалізацією зазвичай була марною. Ми можемо вимкнути її у тесті бази даних за допомогою налаштування SERIALIZE:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
        "TEST": {
            "SERIALIZE": False,  # 👈
        },
    }
}

Це заощадить трохи часу за кожного циклу тестування, можливо, кілька секунд на великих проєктах.

Починаючи з версії 4.0, Django серіалізуватиме бази даних лише за потреби. Обробник тестів перевіряє всі результати тестів і серіалізує лише ті бази даних, які використовуються для тестів, де serialize_rollback = True.

Ця зміна автоматично пришвидшить більшість проєктів. Окрім того, ми позбудемося зайвого непідтримуваного налаштування.

5. Рекурсивні зворотні виклики on_commit()

З приміток до випуску:

TestCase.captureOnCommitCallbacks тепер охоплює нові зворотні виклики, додані під час виконання зворотних викликів transaction.on_commit().

TestCase.captureOnCommitCallbacks() додався ще у Django 3.2 для тестування зворотних викликів on_commit().

На жаль, один рідкісний випадок був не врахований: коли зворотний виклик on_commit() додає ще один зворотний виклик. Наприклад:

def some_view(request):
    ...
    transaction.on_commit(do_something)
    ...


def do_something():
    ...
    transaction.on_commit(another_action)
    ...


def do_something_else():
    ...

Зазвичай Django добре справляється з такими «рекурсивними» зворотними викликами, належно виконуючи всі зворотні виклики. Але captureOnCommitCallbacks() не міг їх захопити (у нього було одне завдання ...).

Починаючи з Django 4.0, captureOnCommitCallbacks() обробляє рекурсивні зворотні виклики, даючи змогу правильно виконувати тестування у таких ситуаціях. Така можливість є й у попередніх версіях Django за допомогою пакунка django-capture-on-commit-callbacks.

6. Журналювання обробника тестів

У примітках до випуску вказано дві зміни:

Новий аргумент logger для DiscoverRunner дозволяє застосувати logger у Python для журналювання.

Новий метод DiscoverRunner.log надає спосіб журналювання повідомлень, які використовують DiscoverRunner.logger, або виводить їх до консолі, якщо дії не вказано.

По суті, основний клас обробників тестів Django, DiscoverRunner, тепер за бажанням може використовувати фреймворк для ведення журналу Python. Це корисно для налаштування його виводу, а ще для перевірки користувацьких підкласів обробників та створення тверджень щодо його виводу.

Ось і все! Сподіваємося, що нові можливості Django 4.0 допоможуть вам оптимізувати код та пришвидшити тестування 😊.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація