【问题标题】:Mock instance attributes without instance没有实例的模拟实例属性
【发布时间】:2021-08-10 20:47:06
【问题描述】:

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

# 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 类没有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 时会被覆盖。

【问题讨论】:

  • 测试没有意义。函数bar() 创建一个Foo(1)。为什么要测试创建Foo(2) 的情况?如果传递给Foo() 的参数可以更改,那么这将提供一个测试发生情况的机会。但它不能。也许你应该断言 Foo(var).x == var 代替。
  • @Mark 这是一段更长更复杂代码的示例。我同意,如果我正在测试这段代码,我不会尝试模拟它。
  • 如果这个实例没有在函数中创建,那么也许你可以传入一个mock。如果它在实例中创建的,那么也许您可以模拟导致它以某种方式创建的条件。一般来说,您应该针对接口进行测试,而不是内部实现。如果您需要模拟内部结构,则测试的困难可能在于建议对代码进行重构。
  • 感谢您的cmets

标签: python mocking pytest pytest-mock


【解决方案1】:

进行这种测试的标准方法是模拟整个类,如下所示:

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 类。

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

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

附言

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

您首先使用 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)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-06-29
    • 2012-10-07
    • 2013-02-27
    • 1970-01-01
    • 1970-01-01
    • 2015-02-19
    • 2015-03-22
    • 2014-04-05
    相关资源
    最近更新 更多