没有实例的模拟实例属性

Mock instance attributes without instance

我想通过模拟一个实例的属性来进行测试,但我无法事先访问该实例。没有它我怎么能模拟这个属性?这是我的最小可重现代码。

# 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'

这是有道理的,因为 Foo class 没有 x,只有实例。如果我添加一个 kwarg mocker.patch('test.Foo.x', 2, create=True) 我就会得到这个

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

因为 Foo.x 将被模拟,但当实例稍后设置 self.x = x 时被覆盖。

进行这种测试的标准方法是模拟整个 class,像这样:

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)

因此在这种情况下,您模拟了整个 Foo class。

然后,有这样的语法 mock_foo.return_value.x = 2:其中 mock_foo.return_value 仅表示调用 Foo(1) 时的 return 对象 - 这是您的对象。由于我们模拟了整个 class - __init__ 方法什么都不做 - 所以你需要自己设置 x 属性。

还要注意 mock_foo.assert_called_once_with(1) - 它会检查是否使用正确的参数调用了 Foo(1)

p.s.

为了简化这种模拟,我创建了一个名为 pytest-mock-generator 的 pytest fixture。您可以使用它来帮助您创建模拟和断言。

您首先使用 mg 夹具来分析您的 bar 方法,以便它为您找到相关的模拟:

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

执行此测试方法时,会生成以下代码(将其打印到控制台并将其复制到剪贴板以供快速使用):

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

然后修改测试函数并执行 bar 方法并使用 generate_asserts 功能:

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)

这将为您提供以下输出:

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

你不需要其中的大部分内容,但你可以保留第二行作为断言并修改第三行,以便 x 具有正确的值:

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)