Search code examples
pythonunit-testingpython-unittest.mock

How to get PropertyMock for assertions without original PropertyMock object?


I have a unittest.mock.PropertyMock (docs) object created for unit testing purposes, on an attribute within an object.

I no longer have the reference to the PropertyMock made, as the PropertyMock was monkeypatched in during test setup.

How can I get access to the PropertyMock for assertions, given I have the object with the given property?

from typing import Any
from unittest.mock import PropertyMock

class Foo:
    @property
    def property_1(self) -> int:
        return 1

def make_obj() -> Any:
    """Make some object, not returning a reference to the PropertyMock made inside."""
    my_obj = Foo()
    type(my_obj).property_1 = PropertyMock(return_value=100)
    # NOTE: function doesn't return the PropertyMock, only the object
    return my_obj

def test_make_obj() -> None:
    made_obj = make_obj()

    # We can see the PropertyMock is in place and works
    assert made_obj.property_1 == 100

    # How can I assert the property was set with 9001?  NOTE: I don't have access
    # to the PropertyMock made above
    made_obj.property_1 = 9001
    type(made_obj).property_1.assert_called_once_with(9001)  # This fails
    # AttributeError: 'int' object has no attribute 'assert_called_once_with'

In other words:

  1. After monkeypatching in the PropertyMock
  2. Calling type(my_obj).property_1 returns 100
  3. I want it to return the PropertyMock used (for assertions)

Solution

  • This is a bit tricky, because accessing the property mock via the attribute will always return the result of the mock instead of the mock itself. This is due to the way it functions: a PropertyMock overwrites __get__ to return the set value, which will be used as soon as you use the attribute access via the dot notation (or if you use getattr). You cannot overwrite __get__ yourself, because that would destroy the functionality of the PropertyMock.

    You can work around this by directly accessing the __dict__ of your class:

    def test_make_obj() -> None:
        made_obj = make_obj()
        assert made_obj.property_1 == 100
        made_obj.property_1 = 9001
        assert made_obj.property_1 == 9001
        type(made_obj).__dict__["property_1"].assert_called_once_with(9001)
    

    You can probably use some helper function to make this a bit nicer, but you still will need the access via the attribute name, at least I see no other way.

    The only other possibility I can think of is to pass the mock itself, though that is of course somewhat redundant and ugly:

    def make_obj() -> Any:
        my_obj = Foo()
        pmock = PropertyMock(return_value=100)
        type(my_obj).property_1 = pmock
        return my_obj, pmock
    
    def test_make_obj() -> None:
        made_obj, pmock = make_obj()
        assert made_obj.property_1 == 100
        made_obj.property_1 = 9001
        assert made_obj.property_1 == 9001
        pmock.assert_called_once_with(9001)