简体   繁体   中英

Pytest patch fixture not resetting between test functions when using return_value

I have a issue with one of my fixtures, which is doing a patch not resetting between test calls.

The fixture is basically a patch which is wrapper around a object, so I can assert that it has been passed into another function.

The fixture looks like this:

@pytest.fixture
def mock_entities(mocker: MockFixture) -> MagicMock:
    entities = Entities()
    namespace = f"{__name__}.{Entities.__name__}"
    return mocker.patch(namespace, return_value=entities)

Entities is a class that I want to patch, but I want it to function completely like the original, due to the fact that it has property methods, as well as using __len__ . It is declared in the body of a function and the reason I need it mocked is because I'm passing it into another function and I want to assert that it has been passed in correctly. I originally tried "wraps=`, but I wasn't able to get that to work correctly.

Full test code below:

import pytest
from pytest_mock import MockFixture
from unittest.mock import MagicMock, PropertyMock
from typing import List
from pprint import pprint
from unittest.mock import patch

class Entities:

    _entities: List[dict] = []

    def __init__(self, entities: List[dict] = []):
        self._entities = entities

    @property
    def entities(self) -> List[dict]:
        return self._entities

    @entities.setter
    def entities(self, value: List[dict]):
        self._entities = value

    def append(self, value: dict):
        self._entities.append(value)

    def __len__(self) -> int:
        return len(self._entities)

class ApiClient:

    def get_values(self) -> List[dict]:
        # We get values from a API with a pager mechanism here
        pass

class EntitiesCacheClient:

    def get_values(self) -> Entities:
        # We get values from cache here
        pass

    def set_values(sel, values: Entities):
        # We set values to cache here
        pass

class EntityDataSource:

    _api_client: ApiClient = None
    _cache_client: EntitiesCacheClient = None

    def __init__(self) -> None:
        self._api_client = ApiClient()
        self._cache_client = EntitiesCacheClient()

    def get_entities(self) -> Entities:

        entities = self._get_entities_from_cache()
        if entities:
            return entities

        # I want to mock Entities, so that I can assert that it is passed in to the EntitiesCacheClient.set_values()
        entities = Entities()
        api_values = 1 
        while api_values:
            api_values = self._api_client.get_values()
            if not api_values:
                break

            for values in api_values:
                entities.append(values)

        if entities:
            self._save_entities_to_cache(entities)

        return entities

    def _get_entities_from_cache(self) -> Entities:
        return self._cache_client.get_values()

    def _save_entities_to_cache(self, entities: Entities):
        self._cache_client.set_values(entities)


@pytest.fixture
def mock_entities_cache_client(mocker: MockFixture) -> MagicMock:
    namespace = f"{__name__}.{EntitiesCacheClient.__name__}"
    return mocker.patch(namespace, autospec=True).return_value

@pytest.fixture
def mock_api_client(mocker: MockFixture) -> MagicMock:
    namespace = f"{__name__}.{ApiClient.__name__}"
    return mocker.patch(namespace, autospec=True).return_value

@pytest.fixture
def mock_entities(mocker: MockFixture) -> MagicMock:
    entities = Entities()
    namespace = f"{__name__}.{Entities.__name__}"
    return mocker.patch(namespace, return_value=entities)

def test_entity_data_source_entities(mock_entities_cache_client, mock_api_client, mock_entities):

    mock_entities_cache_client.get_values.return_value = None

    expected_entity_1 = {"id": 1, "data": "Hello"}
    expected_entity_2 = {"id": 2, "data": "World"}

    expected_entities_list = [
        expected_entity_1, expected_entity_2
    ]

    mock_api_client.get_values.side_effect = [
        [
            expected_entity_1,
            expected_entity_2,
        ],
        []
    ]

    entity_data_source = EntityDataSource()
    result: Entities = entity_data_source.get_entities()

    mock_entities_cache_client.set_values.assert_called_once_with(mock_entities.return_value)

    assert len(result.entities) == len(expected_entities_list)
    assert result.entities == expected_entities_list

def test_entity_data_source_entities_more_results(mock_entities_cache_client, mock_api_client, mock_entities):

    mock_entities_cache_client.get_values.return_value = None

    expected_entity_1 = {"id": 1, "data": "Hello"}
    expected_entity_2 = {"id": 2, "data": "World"}
    expected_entity_3 = {"id": 3, "data": "How"}
    expected_entity_4 = {"id": 4, "data": "Are"}
    expected_entity_5 = {"id": 5, "data": "You"}
    expected_entity_6 = {"id": 6, "data": "Doing?"}

    expected_entities_list = [
        expected_entity_1, expected_entity_2, expected_entity_3,
        expected_entity_4, expected_entity_5, expected_entity_6
    ]

    mock_api_client.get_values.side_effect = [
        [
            expected_entity_1,
            expected_entity_2,
            expected_entity_3,
            expected_entity_4,
            expected_entity_5,
        ],
        [expected_entity_6],
        []
    ]

    entity_data_source = EntityDataSource()
    result: Entities = entity_data_source.get_entities()

    mock_entities_cache_client.set_values.assert_called_once_with(mock_entities.return_value)

    assert len(result.entities) == len(expected_entities_list)
    assert result.entities == expected_entities_list

On the second test method, the fixture is patching Entities and it has a return_value=Entities() (basically). However, the fixture/mock seems to retain the original Entities from the first test, meaning that it already has 2 records inside the _entities , resulting in a total of 8 records and not the 6 it should have.

>       assert len(result.entities) == len(expected_entities_list)
E       assert 8 == 6
E         -8
E         +6

Why is this happening? I thought when using the pyest-mock and the mocker fixture, there would be no need to reset the mocks as it takes care of that for you

https://pypi.org/project/pytest-mock/

This plugin provides a mocker fixture which is a thin-wrapper around the patching API provided by the mock package. Besides undoing the mocking automatically after the end of the test, it also provides other nice utilities such as spy and stub , and uses pytest introspection when comparing calls.

Does this not extend to the objects assigned to return_value ? How am I supposed to be mocking Entities if this is not the correct way?

You have been a victim of the common pitfull of mutable default arguments . Each time you set the entities property you in fact change the default value of the entities argument, so the next time a new Entities object will be created with an empty argument, this will be used instead of an empty list.

The usual fix is to use a non-mutable placeholder object as default value:

    def __init__(self, entities: List[dict] = None):
        self._entities = entities or []

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM