1

我遇到了以下(边缘?)我不知道如何正确处理的情况。一般的问题是

  • 我有一个要测试的功能
  • 在那个函数中,我调用一个以生成器理解作为参数的外部函数
  • 在我的测试中,我模拟了外部函数
  • 现在产品代码和测试代码不同:在产品中,生成器被消耗,模拟不这样做

这是我的代码库中的简化示例:

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

->exitcoverage.py 中的语法解释。 鉴于理解可以执行我确实想要运行的相关业务逻辑,因此错过的覆盖范围是相关的。它只是random.randint在这里调用,但它可以做任何事情。


解决方法:

  1. 我可以只使用列表推导。代码被调用,每个人都很高兴。除了我,他必须修改他们的后端才能缓和测试。
  2. 我可以在测试期间进入模拟,抓住调用 arg,然后手动展开它。这可能看起来很糟糕。
  3. 我可以对函数进行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
4

2 回答 2

2

您是正确的,这里缺少覆盖范围:实际上,由于从未消耗过累积,您甚至可以拥有:

itertools.accumulate(ERRORERRORERROR for _ in range(10))

而且您现有的测试仍然会通过(明显的错误只是被嘲笑了)。

为了解决这个问题,使用side_effect模拟的:

my_mocked_func = mocker.patch('itertools.accumulate', side_effect=list)

当使用可调用对象作为模拟side_effect对象时,它会使用与模拟对象相同的参数来调用,并且此可调用对象的返回值用作模拟对象的返回值(注意:这意味着您也可以在这里断言返回值而不是而不仅仅是直言不讳的call_count断言)。

这将允许您使用生成器并在此处获得 100% 的覆盖率。

于 2020-03-25T02:44:59.343 回答
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")
于 2020-03-24T15:11:35.550 回答