Search code examples
python-unittestpython-unittest.mock

Python unittest create mock class


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.


Solution

  • 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.