如何指导魔术模拟如何处理其参数
How to instruct a magic mock on how it should treat its arguments
我 运行 遇到了以下(边缘?)案例,我不知道如何正确处理。一般的问题是
- 我有一个功能要测试
- 在该函数中,我调用了一个以生成器推导式作为参数的外部函数
- 在我的测试中,我模拟了外部函数
- 现在产品代码和测试代码不同:在产品中,生成器被消耗,模拟不这样做
这是我的代码库中的简化示例:
import itertools
import random
def my_side_effects():
# imaginge itertools.accumulate was some expensive strange function
# that consumes an iterable
itertools.accumulate(random.randint(1, 5) for _ in range(10))
def test_my_side_effects(mocker):
my_mocked_func = mocker.patch('itertools.accumulate')
my_side_effects()
# make sure that side-effects took place. can't do much else.
assert my_mocked_func.call_count == 1
测试 运行 很好,并且对我所关心的一切都足够好。但是当我运行coverage
上代码的时候,我在摘要中描述的情况就很明显了:
----------- coverage: platform linux, python 3.8.0-final-0 -----------
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------------------------------
[...]
my_test_case.py 5 0 2 1 86% 6->exit
[...]
----------------------------------------------------------------------------------
# something like this, the ->exit part on the external call is the relevant part
鉴于理解可以执行我实际上想要 运行 的相关业务逻辑,因此遗漏的覆盖范围是相关的。它只是在这里调用 random.randint
,但它 可以 做任何事情。
解决方法:
- 我可以改用列表理解。代码被调用,每个人都很高兴。除了我,谁必须修改他们的后端以缓和测试。
- 我可以在测试期间将手伸入 mock 中,抓住 call arg,然后用手展开它。这可能看起来很糟糕。
- 我可以对函数进行 monkeypatch 而不是使用 magicmock,像
monkeypatch.setattr('itertools.accumulate', lambda x: [*x])
这样的描述性很强。但是我会失去像我的示例中那样进行调用断言的能力。
我认为好的解决方案应该是这样的,遗憾的是它不存在:
def test_my_side_effects(mocker):
my_mocked_func = mocker.patch('itertools.accumulate')
# could also take "await", and assign treatments by keyword
my_mocked_func.arg_treatment('unroll')
my_side_effects()
# make sure that side-effects took place. can't do much else.
assert my_mocked_func.call_count == 1
老办法:
import itertools
def func():
return list(itertools.izip(["a", "b", "c"], [1, 2, 3]))
def test_mock():
callargs = []
def mock_zip(*args):
callargs.append(args)
for arg in args:
list(arg)
yield ("a", 1)
yield ("b", 2)
old_izip = itertools.izip
itertools.izip = mock_zip
result = func()
itertools.izip = old_izip
assert 1 == len(callargs), "oops, not called once"
assert result == [("a", 1), ("b", 2)], "oops, wrong result"
print("success")
你是对的,这里缺少覆盖:事实上,因为 accumulate 从未消耗过,你甚至可以:
itertools.accumulate(ERRORERRORERROR for _ in range(10))
并且您现有的测试仍然会通过(明显的错误只是被模拟掉了)。
要解决此问题,请使用模拟的 side_effect
:
my_mocked_func = mocker.patch('itertools.accumulate', side_effect=list)
当将可调用对象用作模拟的 side_effect
时,它会使用与模拟对象相同的参数进行调用,并且此可调用对象的 return 值用作 return 值模拟的(注意:这意味着您还可以在此处断言 returned 值,而不仅仅是直截了当的 call_count
断言)。
这将使您能够消耗发电机并在此处获得 100% 的覆盖率。
我 运行 遇到了以下(边缘?)案例,我不知道如何正确处理。一般的问题是
- 我有一个功能要测试
- 在该函数中,我调用了一个以生成器推导式作为参数的外部函数
- 在我的测试中,我模拟了外部函数
- 现在产品代码和测试代码不同:在产品中,生成器被消耗,模拟不这样做
这是我的代码库中的简化示例:
import itertools
import random
def my_side_effects():
# imaginge itertools.accumulate was some expensive strange function
# that consumes an iterable
itertools.accumulate(random.randint(1, 5) for _ in range(10))
def test_my_side_effects(mocker):
my_mocked_func = mocker.patch('itertools.accumulate')
my_side_effects()
# make sure that side-effects took place. can't do much else.
assert my_mocked_func.call_count == 1
测试 运行 很好,并且对我所关心的一切都足够好。但是当我运行coverage
上代码的时候,我在摘要中描述的情况就很明显了:
----------- coverage: platform linux, python 3.8.0-final-0 -----------
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------------------------------
[...]
my_test_case.py 5 0 2 1 86% 6->exit
[...]
----------------------------------------------------------------------------------
# something like this, the ->exit part on the external call is the relevant part
random.randint
,但它 可以 做任何事情。
解决方法:
- 我可以改用列表理解。代码被调用,每个人都很高兴。除了我,谁必须修改他们的后端以缓和测试。
- 我可以在测试期间将手伸入 mock 中,抓住 call arg,然后用手展开它。这可能看起来很糟糕。
- 我可以对函数进行 monkeypatch 而不是使用 magicmock,像
monkeypatch.setattr('itertools.accumulate', lambda x: [*x])
这样的描述性很强。但是我会失去像我的示例中那样进行调用断言的能力。
我认为好的解决方案应该是这样的,遗憾的是它不存在:
def test_my_side_effects(mocker):
my_mocked_func = mocker.patch('itertools.accumulate')
# could also take "await", and assign treatments by keyword
my_mocked_func.arg_treatment('unroll')
my_side_effects()
# make sure that side-effects took place. can't do much else.
assert my_mocked_func.call_count == 1
老办法:
import itertools
def func():
return list(itertools.izip(["a", "b", "c"], [1, 2, 3]))
def test_mock():
callargs = []
def mock_zip(*args):
callargs.append(args)
for arg in args:
list(arg)
yield ("a", 1)
yield ("b", 2)
old_izip = itertools.izip
itertools.izip = mock_zip
result = func()
itertools.izip = old_izip
assert 1 == len(callargs), "oops, not called once"
assert result == [("a", 1), ("b", 2)], "oops, wrong result"
print("success")
你是对的,这里缺少覆盖:事实上,因为 accumulate 从未消耗过,你甚至可以:
itertools.accumulate(ERRORERRORERROR for _ in range(10))
并且您现有的测试仍然会通过(明显的错误只是被模拟掉了)。
要解决此问题,请使用模拟的 side_effect
:
my_mocked_func = mocker.patch('itertools.accumulate', side_effect=list)
当将可调用对象用作模拟的 side_effect
时,它会使用与模拟对象相同的参数进行调用,并且此可调用对象的 return 值用作 return 值模拟的(注意:这意味着您还可以在此处断言 returned 值,而不仅仅是直截了当的 call_count
断言)。
这将使您能够消耗发电机并在此处获得 100% 的覆盖率。