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