Home > database >  How to mock a class with nested properties and autospec?
How to mock a class with nested properties and autospec?

Time:12-02

I'm wondering if it's possible to mock a class which contains properties by using patch and autospec? The goal in the example below is to mock (recursively) ClassB.

Example:

# file: class_c.py

class ClassC:
    def get_default(self) -> list[int]:
        return [1, 2, 3]

    def delete(self, name: str):
        print(f"delete {name}")


# ----------------------
# file: class_b.py
from class_c import ClassC

class ClassB:
    def __init__(self) -> None:
        self._ds = ClassC()

    @property
    def class_c(self) -> ClassC:
        return self._ds

    def from_config(self, cred: str) -> str:
        return cred

# ----------------------
# file: class_a.py

from class_b import ClassB

class ClassA:
    def __init__(self):
        self._client = ClassB()

    @property
    def class_b(self) -> ClassB:
        return self._client

    def test(self, cred: str) -> str:
        return cred

# ----------------------
# file: test.py

import pytest
from unittest import mock

@mock.patch("class_a.ClassB", autospec=True)
def test_class_a(_):
  class_a = ClassA()
  with pytest.raises(TypeError):
      class_a.class_b.from_config()  # ✅ raised - missing 1 required positional argument: 'cred' 
  with pytest.raises(TypeError):
      class_a.class_b.class_c.delete()  # <- ❌ Problem - should raise exception since autospec=True 

The property class_c of the class ClassB is not mocked properly. I would expect TypeError when trying to call delete() without any argument

I've tried several things but without success. Any idea?

EDIT:

The code is just an example, and the test function was just written to demonstrate the expected behaviour. ClassB can be seen as a third-party service which needs to be mocked.

EDIT2:

Additionally to the accepted answer, I would propose to use PropertyMock for mocking properties:

def test_class_a():
    class_b_mock = create_autospec(class_a.ClassB)
    class_c_mock = create_autospec(class_b.ClassC)
    type(class_b).class_c = PropertyMock(return_value=class_c_mock)
    with mock.patch("class_a.ClassB", return_value=class_b_mock):
        class_a_instance = ClassA()
        with pytest.raises(TypeError):
            class_a_instance.class_b.from_config()
        with pytest.raises(TypeError):
            class_a_instance.class_b.class_c.delete()

CodePudding user response:

Once you patched the target class, anything you try to access under that class will be mocked with MagicMock (also recursively). Therefore, if you want to keep the specification of that class, then yes, you should use the autospec=true flag. But because you are trying to mock a class within a class accessed by a property, You have to keep the specification of each class you want to test:

def test_class_a():
    class_b_mock = create_autospec(class_a.ClassB)
    class_c_mock = create_autospec(class_b.ClassC)
    class_b_mock.class_c = class_c_mock
    with mock.patch("class_a.ClassB", return_value=class_b_mock):
        class_a_instance = ClassA()
        with pytest.raises(TypeError):
            class_a_instance.class_b.from_config()
        with pytest.raises(TypeError):
            class_a_instance.class_b.class_c.delete()
  • Related