Laurent
Laurent

Reputation: 13518

How to patch objects with "autospec" set to True?

I have the following toy class:

class MyClass:

    def __init__(self, x):
        self.x = x

    def get_operator(self):
        answer = input("Multiply? ")
        if answer == "y":
            return "multiply"

    def multiply(self, y):
        if self.get_operator() == "multiply":
            return self.x * y

The following test (using pytest) will throw an error:

def test_multiply_is_called(mocker):
    multiply = mocker.patch("package.module.MyClass.multiply", return_value=1, autospec=True)
    my_instance = MyClass(2)
    my_instance.multiply(3)
    multiply.assert_called_once()  # no error whatever autospec is equal to (True or False)
    multiply.assert_called_with(3)  # no error only if autospec=False

TypeError: Can't use 'autospec' with create=True

It gets even less clear when trying to mock Python built-in function input:

def test_input_is_called_once(mocker):
    input = mocker.patch("package.module.input", return_value="y", autospec=True)
    my_instance = MyClass(2)
    my_instance.get_operator()
    input.assert_called_once()  # no error only if autospec=False

E AssertionError: expected call not found.
E Expected: multiply(3)
E Actual: multiply(<package.module.MyClass object at 0x0000022A4BEEFD00>, 3)

I get that mocking with autospec=True is a recommended practice, but, obviously, I have a wrong understanding of how it works, despite having read this post and this one, for instance.

Could someone please clarify this subject?

Upvotes: 1

Views: 1525

Answers (1)

MrBean Bremen
MrBean Bremen

Reputation: 16855

These are actually two unrelated problems. The first problem stems from the fact that with autospec=False, the call my_instance.multiply(3) just boils down to a call of the mock with the argument 3 without any checks (e.g. a simple function call). If you use autospec=True, however, the method multiply is bound to my_instance as in the real call, and called as a method, with my_instance as the first (self) argument. So to get it to work you need:

def test_multiply_is_called(mocker):
    multiply = mocker.patch("package.module.MyClass.multiply", return_value=1, autospec=True)
    my_instance = MyClass(2)
    my_instance.multiply(3)
    multiply.assert_called_with(my_instance, 3)

The second case is different, and I have to admit that I'm not completely clear how it works correctly with autospec=False. The problem is that input is not bound to the module, but it is a global import (from builtins), and so it is created as a new mock object without the knowledge of the actual built-in function. In this case, autospec will not work because of missing information. This can be fixed by mocking the built-in function instead:

def test_input_is_called_once(mocker):
    input = mocker.patch("builtins.input", return_value="y", autospec=True)
    my_instance = MyClass(2)
    my_instance.get_operator()
    input.assert_called_once()

This works for both autospec=True and autospec=False. As far as I understand, with autospec=False it works because a mock is created for a local input function, and that is then actually used in the call. From my point of view, the correct way for mocking input is to mock builtins.input, anyway.

Upvotes: 1

Related Questions