В этом тесте определите результат второго запуска параметризации и объясните причину.
import pytest
shared = {"items": []}
@pytest.mark.parametrize("case", [shared, shared])
def test_append(case):
case["items"].append("x")
assert len(case["items"]) == 1
Первый случай пройдёт, а второй завершится ошибкой: длина списка во втором запуске будет равна 2, а не 1. pytest не копирует значения параметров: оба тестовых случая получают ссылку на один и тот же словарь shared.
Параметризация появилась как способ запускать один тестовый сценарий с несколькими входными данными без копирования тела теста. Для эффективности pytest передаёт параметры непосредственно тестовой функции, не выполняя неявное глубокое копирование объектов.
Такой подход сохраняет произвольные Python-объекты и не навязывает универсальную стратегию копирования. Обратная сторона — тест сам отвечает за то, чтобы не изменять разделяемые изменяемые данные.
В примере список case["items"] изменяется внутри теста. Поскольку оба элемента списка параметров ссылаются на один объект shared, изменение первого запуска сохраняется ко второму.
Это создаёт зависимость между тестовыми случаями: результат может зависеть от порядка выполнения, а ошибка будет выглядеть как проблема второго набора данных, хотя фактическая причина находится в мутации первого.
При параметризации pytest формирует отдельный запуск теста для каждого элемента списка параметров, но сам объект параметра обычно передаётся по исходной ссылке. В данном случае выполняется логика, эквивалентная такой:
Исправление зависит от источника данных. Если значения известны заранее, нужно создать отдельный изменяемый объект для каждого случая:
Если параметры генерируются программно, можно создавать новый объект на каждой итерации. copy.deepcopy подходит, когда требуется независимая копия вложенной структуры, но он может быть дорогим или неприменимым для объектов с внешними ресурсами.
Лучший компромисс — не изменять входной параметр в тесте. Если тестируемый код обязан изменить объект, передавайте ему копию или создавайте данные внутри фабрики. Важно также не путать независимые запуски pytest с изоляцией объектов: отдельный запуск не означает автоматическое клонирование параметра.
В тестах обработчика заказов параметром был словарь заказа. Один сценарий добавлял в него вычисленное поле, после чего следующий сценарий неожиданно получал уже изменённый заказ.
Рассматривались три варианта. Глубокое копирование в начале теста давало изоляцию, но скрывало факт мутации и увеличивало стоимость тестов. Использование одного общего словаря было проще, но делало сценарии зависимыми. Создание отдельного словаря в генераторе параметров явно формировало независимые входы.
Выбрали генератор, возвращающий новый словарь для каждого сценария, а тестируемый код дополнительно проверили на отсутствие нежелательной мутации входа. В результате порядок запусков перестал влиять на результат, а данные каждого сценария стали видны непосредственно в объявлении параметров.
Нет. Изменяемый объект передаётся как есть; pytest не выполняет для него ни поверхностное, ни глубокое копирование автоматически. Поэтому повторное использование одного объекта в параметрах или его изменение внутри теста может связать несколько случаев.
Да, в конструкции вроде [{"items": []}, {"items": []}] это два разных словаря и два разных вложенных списка. Однако это не означает общей гарантии для более сложных выражений: если вложенный объект вынесен в переменную и используется повторно, ссылка снова будет общей.
copy.copy для вложенного параметра?Не всегда. Поверхностная копия создаёт новый внешний контейнер, но вложенные списки и словари сохраняют прежние ссылки. Если тест изменяет вложенную структуру, нужна независимая сборка данных или copy.deepcopy; выбор зависит от типов объектов и стоимости копирования.