насмешник.патч использует данные из предыдущего параметризованного запуска

#python #pandas #pytest #pytest-mock

Вопрос:

Я новичок в pytest, поэтому могу неправильно использовать некоторую семантику pytest.

В общем, у меня возникла следующая проблема:

Я использую mark.parametrize для издевательства над тестом, и когда я использую ту же переменную в аргументе, издевательство использует данные предыдущего запуска вместо того, что я указываю.

Аналитически:

На первой «итерации» в mark.parametrize я использую mock_data_1 для макетирования getData.get_data(). Затем тест, как я и ожидал, высмеивает data здесь: data = GetData.get_data() а затем добавляет новый столбец к данным data['new_col0'] .

На второй «итерации», где в mark.parametrize я снова использую mock_data_1, вместо того, чтобы иметь новый свежий набор mock_data_1, тест использует предыдущие данные, содержащие дополнительный столбец.

Вот некоторые примеры файлов:

file.py

 from test_file_get_data import GetData

class MyClass:
    def new_dataset(arg):
        data = GetData.get_data(arg)  # Mock this part
        data[f'new_col{arg}'] = arg  # New column to data
        return data
 

test_file.py

 from file import MyClass
import pandas as pd
import pytest

class TestMyClass:
    mock_data_1 = pd.DataFrame({"col_1": [1,2,3]})
    arg_1 = 0
    arg_2 = 1
    output_1 = pd.DataFrame({"col_1": [1,2,3], "new_col0": [0,0,0]})
    output_2 = pd.DataFrame({"col_1": [1,2,3], "new_col1": [1,1,1]})

    @pytest.mark.parametrize(
        'mock_arguments, arg, result',
        [
            (mock_data_1, arg_1, output_1),
            (mock_data_1, arg_2, output_2)
        ]
    )
    def test_new_dataset(self, mocker, mock_arguments, arg, result):
       mocker.patch(
            'file.GetData.get_data',
            return_value=mock_arguments,
        )
       print(mock_arguments)
       res = MyClass.new_dataset(arg)
       print(res)
       assert res.to_dict() == result.to_dict()

 

test_file_get_data.py

 import pandas as pd

class GetData:
    def get_data(arg):
        data = pd.DataFrame({"a":[1, 2, 3]})
        return data
 

Таким образом, первый тест проходит, но второй завершается неудачей, потому что возвращаемые данные таковы:

 {'col_1': {1, 2, 3},
 'new_col0': {0, 0, 0},
 'new_col1': {1, 1, 1}}
 

вместо этого:

 {'col_1': {1, 2, 3},
 'new_col1': {1, 1, 1}}
 

Эту проблему можно решить , если я заменю data = GetData.get_data() на data = GetData.get_data().copy() , но я предполагаю, что делаю что-то не так в тестах.

Разве данные не должны обновляться и/или удаляться после каждой итерации? Или то, что происходит, является ожидаемым поведением?

Комментарии:

1. ваш код в написанном виде не запускается, пожалуйста, исправьте свой пример

2. Помимо того факта, что код не может выполняться как есть, вы не показываете, что GetData на самом деле происходит. Я предполагаю, что он возвращает глобальный диктант, которым будет управлять каждый вызов, чтобы поведение было ожидаемым.

3. Мои извинения, теперь я добавил воспроизводимый пример.

Ответ №1:

Как упоминалось в комментарии, проблема заключается в том, что глобальная переменная (в данном случае переменная класса, но это не меняет поведения) используется в тесте, изменяется внутри теста, а затем измененная переменная используется в следующем тесте. Нет ничего, что указывало pytest бы на то, что переменная должна быть сброшена — сброс переменных обычно выполняется в приспособлениях.

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

 class TestMyClass:

    arg_1 = 0
    arg_2 = 1
    output_1 = pd.DataFrame({"col_1": [1, 2, 3], "new_col0": [0, 0, 0]})
    output_2 = pd.DataFrame({"col_1": [1, 2, 3], "new_col1": [1, 1, 1]})

    @pytest.fixture
    def mock_arguments(self):
        return pd.DataFrame({"col_1": [1, 2, 3]})


    @pytest.mark.parametrize(
        'arg, result',
        [
            (arg_1, output_1),
            (arg_2, output_2)
        ]
    )
    def test_new_dataset(self, mocker, mock_arguments, arg, result):
        mocker.patch(
            'file.GetData.get_data',
            return_value=mock_arguments,
        )
        print(mock_arguments)
        ...
 

Это стандартный способ обработки сброса переменной pytest .

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

     @pytest.mark.parametrize(
        'mock_arguments, arg, result',
        [
            (mock_data_1, arg_1, output_1),
            (mock_data_1, arg_2, output_2)
        ]
    )
    def test_new_dataset(self, mocker, mock_arguments, arg, result):
        mocker.patch(
            'file.GetData.get_data',
            return_value=mock_arguments.copy(),  # use a copy of the value
        )
        print(mock_arguments)
        ...