Программирование PythonТестированиеPython-разработчик, пишущий и поддерживающий автоматизированные тесты

В этом тесте определите результат второго запуска параметризации и объясните причину. пример с кодом

В этом тесте определите результат второго запуска параметризации и объясните причину.

import pytest

shared = {"items": []}

@pytest.mark.parametrize("case", [shared, shared])
def test_append(case):
    case["items"].append("x")
    assert len(case["items"]) == 1
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Первый случай пройдёт, а второй завершится ошибкой: длина списка во втором запуске будет равна 2, а не 1. pytest не копирует значения параметров: оба тестовых случая получают ссылку на один и тот же словарь shared.

Исторический контекст

Параметризация появилась как способ запускать один тестовый сценарий с несколькими входными данными без копирования тела теста. Для эффективности pytest передаёт параметры непосредственно тестовой функции, не выполняя неявное глубокое копирование объектов.

Такой подход сохраняет произвольные Python-объекты и не навязывает универсальную стратегию копирования. Обратная сторона — тест сам отвечает за то, чтобы не изменять разделяемые изменяемые данные.

Постановка проблемы

В примере список case["items"] изменяется внутри теста. Поскольку оба элемента списка параметров ссылаются на один объект shared, изменение первого запуска сохраняется ко второму.

Это создаёт зависимость между тестовыми случаями: результат может зависеть от порядка выполнения, а ошибка будет выглядеть как проблема второго набора данных, хотя фактическая причина находится в мутации первого.

Подробное решение

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

first_case = shared second_case = shared first_case["items"].append("x") # список содержит один элемент second_case["items"].append("x") # тот же список содержит два элемента

Исправление зависит от источника данных. Если значения известны заранее, нужно создать отдельный изменяемый объект для каждого случая:

@pytest.mark.parametrize("case", [ {"items": []}, {"items": []}, ]) def test_append(case): case["items"].append("x") assert len(case["items"]) == 1

Если параметры генерируются программно, можно создавать новый объект на каждой итерации. copy.deepcopy подходит, когда требуется независимая копия вложенной структуры, но он может быть дорогим или неприменимым для объектов с внешними ресурсами.

Лучший компромисс — не изменять входной параметр в тесте. Если тестируемый код обязан изменить объект, передавайте ему копию или создавайте данные внутри фабрики. Важно также не путать независимые запуски pytest с изоляцией объектов: отдельный запуск не означает автоматическое клонирование параметра.

Ситуация из практики

В тестах обработчика заказов параметром был словарь заказа. Один сценарий добавлял в него вычисленное поле, после чего следующий сценарий неожиданно получал уже изменённый заказ.

Рассматривались три варианта. Глубокое копирование в начале теста давало изоляцию, но скрывало факт мутации и увеличивало стоимость тестов. Использование одного общего словаря было проще, но делало сценарии зависимыми. Создание отдельного словаря в генераторе параметров явно формировало независимые входы.

Выбрали генератор, возвращающий новый словарь для каждого сценария, а тестируемый код дополнительно проверили на отсутствие нежелательной мутации входа. В результате порядок запусков перестал влиять на результат, а данные каждого сценария стали видны непосредственно в объявлении параметров.

Что кандидаты часто упускают

  1. Создаёт ли pytest копию параметра, если параметр — список или словарь?

Нет. Изменяемый объект передаётся как есть; pytest не выполняет для него ни поверхностное, ни глубокое копирование автоматически. Поэтому повторное использование одного объекта в параметрах или его изменение внутри теста может связать несколько случаев.

  1. Будут ли независимыми два одинаково записанных литерала словаря?

Да, в конструкции вроде [{"items": []}, {"items": []}] это два разных словаря и два разных вложенных списка. Однако это не означает общей гарантии для более сложных выражений: если вложенный объект вынесен в переменную и используется повторно, ссылка снова будет общей.

  1. Достаточно ли вызвать copy.copy для вложенного параметра?

Не всегда. Поверхностная копия создаёт новый внешний контейнер, но вложенные списки и словари сохраняют прежние ссылки. Если тест изменяет вложенную структуру, нужна независимая сборка данных или copy.deepcopy; выбор зависит от типов объектов и стоимости копирования.