pytest.parametrize: почему один тест меняет данные другого

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

Параметр передаётся как объект

pytest передаёт значения параметров без автоматического копирования. Если два набора параметров содержат ссылку на один список, они обращаются к общей памяти. Изменение через pop, sort или вложенный словарь останется доступным следующему тесту. Зависимость от порядка — следствие общего объекта, а не особенность планировщика тестов.

Сначала определите договорённость функции. Обработчик очереди вправе извлекать элементы из переданного списка; тогда тест должен выдавать ему новый список. Если же функция обещает сохранять вход, вместо защитной копии закрепите это свойство отдельным assert. Копирование в тесте не должно скрывать нарушение API.

Хранить описание входа отдельно

В примере параметр содержит неизменяемый кортеж номеров. Каждый вызов теста строит из него новый список. Два одинаковых набора здесь намеренны: они проверяют повторяемость для одинакового описания. Имена ids позволяют отличить независимые запуски в отчёте.

Для вложенных объектов list(tuple_value) недостаточно, если кортеж хранит изменяемые словари. В таком случае удобнее фабрика, которая создаёт всю структуру заново. deepcopy тоже подходит не для любого объекта: открытый файл или соединение не становятся независимыми ресурсами от попытки глубокого копирования.

import pytest

def drain(queue):
    result = []
    while queue:
        result.append(queue.pop(0) * 10)
    return result

@pytest.mark.parametrize("source,expected", [
    ((1, 2), [10, 20]),
    ((1, 2), [10, 20]),
    ((), []),
], ids=["first-run", "repeat-same-input", "empty"])
def test_drain_has_fresh_input(source, expected):
    queue = list(source)
    assert drain(queue) == expected
    assert queue == []
    assert source in ((1, 2), ())

Поймать проблему до расширения набора

При подозрении на общее состояние запустите каждый узел отдельно, затем вместе. Посмотрите, где создаётся объект: на уровне модуля, в session-фикстуре или внутри теста. Исправление должно поставить границу жизни там, где нужна независимость. Увеличение числа случайных примеров эту границу не создаёт.

Ожидаемые значения также лучше не изменять в проверяемом коде. Здесь список expected только читается. Если функция принимает и изменяет ожидаемую структуру, тест может начать сравнивать два уже испорченных объекта. Независимый расчёт ожидания важнее количества строк в таблице параметров.

Практика с понятными границами

Начните с открытого урока первых тестов pytest: обычный вход, пустой вход, отказ. Затем превратите их в параметры и проверьте независимость данных. Откройте программу, чтобы продолжить параметризацией и областями видимости фикстур.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

    Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.