1

As the unittest.mock documentation points out:

Because of the way mock attributes are stored you can’t directly attach a PropertyMock to a mock object. Instead you can attach it to the mock type object

However, I'm encountering cases where I want to mock out multiple values of a property on multiple class instances within a single test.

For example, say I have a TimeSlot class with an availability property:

class TimeSlot:
    @property
    def availability(self):
        # Run a complex DB query to determine availability

... and I have a helper class that sorts a list of TimeSlot instances by availability:

from unittest.mock import Mock

from app.helpers import sort_slots
from app.models import TimeSlot


def test_sort_slots_reorders_by_descending_availability():
    slot_a = TimeSlot()
    slot_a.availability = Mock(return_value=0.1) # BOOM!

    slot_b = TimeSlot()
    slot_b.availability = Mock(return_value=0.2)

    slots = [slot_a, slot_b]

    assert sort_slots(slots) == [slot_b, slot_a]

Running this test raises an error when I try to assign a Mock (or PropertyMock) to the availability property:

AttributeError: can't set attribute

Is there any way to mock property return values per instance of a Python class?

If not, is there a different way I should be approaching writing unit tests like this?

Thanks!

vthomas2007
  • 153
  • 6

2 Answers2

3

The availability property is defined on the TimeSlot class, not instances, so you need to replace it with something at the class level. In this example, I replace it with a class attribute that I then override with instance attributes.

from unittest.mock import patch

class TimeSlot:
    @property
    def availability(self):
        return 0.0

slot1 = TimeSlot()
slot2 = TimeSlot()
with patch('__main__.TimeSlot.availability', 0.0):
    slot1.availability = 0.1
    slot2.availability = 0.2

    assert slot1.availability == 0.1
    assert slot2.availability == 0.2

When the with block ends, the original availability property will be put back in place.

Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
  • note: patch('__main__.TimeSlot.availability', 0.0) was not working for me. I had to go for the full module path instead of __main__ – Skratt Sep 21 '21 at 10:36
  • In this example, @Skratt, `__main__` is the full module path. You're right, though, if I were patching the `myapp.timeslot.TimeSlot` class, then I would need to patch `myapp.timeslot.TimeSlot.availability`. – Don Kirkby Sep 21 '21 at 19:22
0

You should be mocking the TimeSlot object itself. This allows to change how the availability property behaves.

RvdK
  • 19,580
  • 4
  • 64
  • 107