2

I would like to test by mocking an instance's attribute, but I do not have access to the instance beforehand. How can I mock the attribute without it? Here is my minimum reproducible code.

# test.py

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

def bar():
    return Foo(1).x + 1

def test_bar(mocker):
    mocker.patch('test.Foo.x', 2)
    assert bar() == 3
$ pytest test.py
FAILED test.py::test_bar - AttributeError: <class 'test.Foo'> does not have the attribute 'x'

This makes sense since the Foo class doesn't have an x, only instances. If I add a kwarg mocker.patch('test.Foo.x', 2, create=True) I then get this

$ pytest test.py
FAILED test.py::test_bar - assert 2 == 3

since Foo.x will get mocked but overridden when the instance later sets self.x = x.

spagh-eddie
  • 124
  • 9
  • The test doesn't really make sense. The function `bar()` creates a `Foo(1)`. Why are you testing a situation where it creates a `Foo(2)`? If the parameter passed to `Foo()` can change, then that would provide an opportunity to test what happens. But it can't. Maybe you should be asserting that `Foo(var).x == var` instead. – Mark Aug 10 '21 at 20:54
  • @Mark this is intended as an example for a longer more complicated piece of code. I agree that if I were testing this code I would not try to mock it. – spagh-eddie Aug 10 '21 at 21:20
  • If this instance is not created in the function, then maybe you can pass in a mock. If it *is* created in the instance, then maybe you can mock the conditions that cause it to get created a certain way. In general, you should be testing against the interface, not the internal implementation. If you need to mock internals, it's possible the difficulty in testing is suggesting a refactor of the code. – Mark Aug 10 '21 at 22:13

1 Answers1

0

The standard way to do this kind of test is to mock the entire class, like so:

def test_bar(mocker):
  mock_foo = mocker.MagicMock(name='Foo')
  mocker.patch('test.Foo', new=mock_foo)
  mock_foo.return_value.x = 2

  assert bar() == 3
  mock_foo.assert_called_once_with(1)

So in this case, you mock the entire Foo class.

Then, there is this syntax mock_foo.return_value.x = 2: where mock_foo.return_value simply means the return object when calling Foo(1) - which is your object. Since we mocked the entire class - the __init__ method does nothing - so you need to set the x attribute on your own.

Also note the mock_foo.assert_called_once_with(1) - which checks that Foo(1) was called with the right parameter.

p.s.

To simplify this kind of mocking, I created a pytest fixture called pytest-mock-generator. You can use it to help you create the mocks and asserts.

You start with using the mg fixture to analyze your bar method so it locates the relevant mocks for you:

def test_bar(mocker, mg):
  mg.generate_uut_mocks(bar)

When this test method is executed, it generates the following code (prints it to the console and copies it to your clipboard for quick usage):

# mocked dependencies
mock_Foo = mocker.MagicMock(name='Foo')
mocker.patch('test.Foo', new=mock_Foo)

You then modify the test function and execute the bar method and use the generate_asserts capability:

def test_bar(mocker, mg):
  # mocked dependencies
  mock_Foo = mocker.MagicMock(name='Foo')
  mocker.patch('test.Foo', new=mock_Foo)

  bar()

  mg.generate_asserts(mock_Foo)

This gives you the following output:

assert 1 == mock_Foo.call_count
mock_Foo.assert_called_once_with(1)
mock_Foo.return_value.x.__add__.assert_called_once_with(1)

You don't need most of it, but you can keep the second line for your assert and the modify the third line so x has the right value:

def test_bar(mocker):
  mock_foo = mocker.MagicMock(name='Foo')
  mocker.patch('test.Foo', new=mock_foo)
  mock_foo.return_value.x = 2

  assert bar() == 3
  mock_foo.assert_called_once_with(1)
Peter K
  • 1,959
  • 1
  • 16
  • 22