0

The Case:
I've a class View in my code that creates an instance during execution, which one I want to mock.
I'm passing implementation of View before running the code.

class View:
    def __init__(arg):
        self.arg = arg
        self.inner_class = self.InnerClass()

item to test:

view.inner_class.some_method.assert_called_once()

The Problem:
I can't properly create a MockView class to get correct mock_view_instance during execution.

I've tried 1:

  • Returns real object (result of Mock call) without assert_called_once method.
mock_instance = Mock(wraps=View, spec=View, spec_set=True)

I've tried 2:

  • inner_class not exists (no entering to __init__ method).
mock_instance = Mock(spec=View, spec_set=True)

I've tried 3:

  • Ok but View isnstance can be instantiated without arg - it's error prone.
  • arg not exists at this moment, it will be defined dynamically during a test call itself.
mock_instance = Mock(spec=View(arg='foo'), spec_set=True)

I've tried 4:

  • TypeError: 'View' object is not callable.
mock_instance = Mock(wraps=View(arg='foo'))

I've tried 5:

  • TypeError: 'NonCallableMagicMock' object is not callable
    (No matter instance=True/False)
mock_instance = create_autospec(spec=View(arg='foo'))

My dirty solution:

# Real object with mock methods (wrapper)
wrapper_instance = Mock(wraps=View, spec_set=True)  

# Return mocked object as instance  
# (don't use "wrapper_instance" as spec, it will cause infinite recursion).
wrapper_instance.side_effect = Mock(spec=Mock(wraps=View))

P.S. I'm preferring not to use patch because it's very implicit.
My architecture allows to set any required object during configuration.

salius
  • 918
  • 1
  • 14
  • 30
  • You never showed the actual unit being tested. I am assuming there is some function you are testing, please include it in the example code. It is not clear whether that function receives an existing instance of `View` as argument or if it creates one in its body. Also I am curious why on earth you consider `patch` to be "very implicit" and what that even means in this context. What is more explicit than saying _"give me a mock of the following object please"_? – Daniil Fajnberg Oct 29 '22 at 10:31

1 Answers1

0

Say we have the following code.py:

class View:
    class InnerClass:
        def some_method(self) -> None:
            print(self.__class__.__name__, "instance says hi")

    def __init__(self, arg: str) -> None:
        self.arg = arg
        self.inner = self.InnerClass()


def f(view: View) -> int:
    view.inner.some_method()
    return 42


def g() -> str:
    view = View(arg="foo")
    view.inner.some_method()
    return view.arg + "bar"

Here is how you might properly unit test the functions f and g:

from unittest import TestCase
from unittest.mock import MagicMock, patch

from . import code


class CodeTestCase(TestCase):
    def test_f(self) -> None:
        mock_some_method = MagicMock()
        mock_view = MagicMock(
            inner=MagicMock(some_method=mock_some_method)
        )

        output = code.f(mock_view)
        self.assertEqual(42, output)
        mock_some_method.assert_called_once_with()

    @patch.object(code, "View")
    def test_g(self, mock_view_cls: MagicMock) -> None:
        mock_arg = "xyz"
        mock_some_method = MagicMock()
        mock_view_cls.return_value = MagicMock(
            arg=mock_arg,
            inner=MagicMock(some_method=mock_some_method),
        )

        output = code.g()
        self.assertEqual(mock_arg + "bar", output)
        mock_view_cls.assert_called_once_with(arg="foo")
        mock_some_method.assert_called_once_with()

To test f, we need to give it an argument that behaves like a View instance in the context of f. So all we need to do is construct a mock object that has all the View attributes needed inside f. The function relies on the inner attribute of View and the presence of some_method on that inner object. We want to ensure that f actually calls that method. Note that what I did was more than necessary and done for readability only. We could have just written the test method like this:

    def test_f(self) -> None:
        mock_view = MagicMock()
        output = code.f(mock_view)
        self.assertEqual(42, output)
        mock_view.inner.some_method.assert_called_once_with()

For testing g we need to mock the entire View class for the duration of the test since we don't want to rely on any implementation details of how it is instantiated. This is where patch shines. We ensure that instead of actually calling View the function calls a mock returning another mock that behaves like a View instance in the context of g. Same logic from then on.

In general this is the prudent approach to unit test. We mock out everything we wrote ourselves that is not part of the unit under testing. The test should be totally agnostic to any implementation details of other units.

It would be a different story, if View was a third-party or built-in class. In that case we would (usually) use it as is under the assumption that the maintainers of that class do their own testing and that it works as advertised.

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41