1

I'm having a real hard time understanding how to mock a class that has a property and setter fixtures that access a "private" attribute.

import pytest
from pytest_mock import MockFixture
from unittest.mock import MagicMock
from typing import List

class MyEntity:

    _my_list: List[str] = []

    def __init__(self, list_vals = []):
        self._my_list = list_vals

    @property
    def my_list(self) -> List[str]:
        return self._my_list

    @my_list.setter
    def my_list(self, value: List[str]):
        self._my_list = value

    def append(self, value: str):
        self._my_list.append(value)


class Foo:

    def generate_entity(self, list_vals: List[str]) -> MyEntity:
        return MyEntity()

    def set_values(self, entity: MyEntity, list_vals: List[str]) -> MyEntity:
        entity.my_list = list_vals
        return entity

    def add_value(self, entity: MyEntity, value: str) -> MyEntity:
        entity.append(value)
        return entity


@pytest.fixture
def mock_my_entity(mocker: MockFixture) -> MagicMock:

    namespace = f"{__name__}.{MyEntity.__name__}"
    mock_my_entity = mocker.patch(namespace, autospec=True)

    return mock_my_entity.return_value

def test_foo(mock_my_entity):

    expect_list_values = ["Hello", "World"]

    foo = Foo()
    entity = foo.generate_entity(expect_list_values)
    assert len(entity._my_list) == 0

    entity = foo.set_values(entity, expect_list_values)
    assert entity._my_list == expect_list_values

    expected_extra_value = "more"
    entity = foo.add_value(entity, expected_extra_value)
    assert entity._my_list == expect_list_values + expected_extra_value

After running the test, this is what I get:

mock_my_entity = <NonCallableMagicMock name='MyEntity()' spec='MyEntity' id='4351847632'>

    def test_foo(mock_my_entity):
    
        expect_list_values = ["Hello", "World"]
    
        foo = Foo()
        entity = foo.generate_entity(expect_list_values)
        assert len(entity._my_list) == 0
    
        entity = foo.set_values(entity, expect_list_values)
>       assert entity._my_list == expect_list_values
E       AssertionError: assert <MagicMock na...='4362862544'> == ['Hello', 'World']
E         Right contains 2 more items, first extra item: 'Hello'
E         Full diff:
E         - <MagicMock name='MyEntity()._my_list' spec='list' id='4362862544'>
E         + ['Hello', 'World']

scratchpad.py:58: AssertionError

I've also tried using PropertyMock in various ways, but nothing seems to work.

How do I get this to work. How do I mock the MyEntity class so that everything works the way it should?

SynackSA
  • 855
  • 1
  • 12
  • 35
  • I'm not completely sure what do you want to achieve. If you replace `MyEntity` with a mock, all of it's methods will just do nothing, but it looks as if you want them to function as before. In your example, there would be no need to mock it at all, but I understand that this is a dumbed down example. Depending on your needs, you can either only mock some of the methods of `MyEntity`, or have to add the wanted behavior to the mock. Also note that `_my_list` is private only per convention, you can access it just like any "public" attribute. – MrBean Bremen Dec 23 '21 at 17:11
  • Also, are you sure you want to test the attribute `_mylist` instead of the public property `my_list` if you mock `MyEntity` anyway? Your mock does not have that attribute. – MrBean Bremen Dec 23 '21 at 17:18
  • @MrBeanBremen I've created the mock/patch, so that I can do asserts that certain functions are called with that as a param. It would be in the same scope as the function that declared it. This dumb down version just duplicates the problem, not so much the way I'm using the classes/objects. – SynackSA Dec 23 '21 at 18:41

1 Answers1

1

I'm still not sure I understand what you want to do, but I'll propose a possibility that could fix the test as shown here, comments inline.

def test_foo(mock_my_entity):
    def append(value):
        mock_my_entity.my_list.append(value)

    expect_list_values = ["Hello", "World"]

    foo = Foo()
    # for `append` in the mock to behave as in the original class,
    # we have to change it
    mock_my_entity.append = append
    entity = foo.generate_entity(expect_list_values)
    assert len(entity.my_list) == 0

    # in this case, the list is directly passed to your class,
    # so I prevent that it will change with changes in the class by copying it
    # in reality, this will probably be done in MyEntity
    entity = foo.set_values(entity, expect_list_values.copy())

    # I changed this and further checks to check for the property 
    # instead of the attribute, as this has the same semantics
    assert entity.my_list == expect_list_values

    expected_extra_value = "more"
    entity = foo.add_value(entity, expected_extra_value)
    assert entity.my_list == expect_list_values + [expected_extra_value]

This will work, but I think this is not what you really need. Just wouldn't fit into the comments...

MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46
  • Yeah, it's not quite what I need. I'll update the original post a bit later to better reflect what I'm doing and what the problem is. Appreciate the effort either way, so have a imaginary internet point, on me :) – SynackSA Dec 24 '21 at 00:22
  • I may have another look after you update the original post... – MrBean Bremen Jan 02 '22 at 09:06
  • 1
    I managed to figure out a way of doing with using `patch` and `return_value`, where I would just return an instance of the objects I wanted, which essentially wrapped my object with a Mock object, so still allowed me to do the assert functions that come with the Mock class. You actually already replied to the post, although the problem was a different one, you can see the way the code changed to have it do what I needed: https://stackoverflow.com/questions/70503503/pytest-patch-fixture-not-resetting-between-test-functions-when-using-return-valu/70503949?noredirect=1#comment124639366_70503949 – SynackSA Jan 03 '22 at 16:40