如何在python中测试一个函数是否调用range?

mgdq6dx1  于 2023-01-29  发布在  Python
关注(0)|答案(1)|浏览(132)

我是一名Python教师,我想给我的学生一个任务:编写一个函数,使用for循环和range对象计算列表的平均值。
我想对他们的函数进行一个测试,看看它是否真的使用了range对象。我该怎么做呢?
应该是这样的:

def avg(L):
    Pass

def test_range(avg):
    ...

如果avg包含range,则test_range应返回True
我尝试过利用func_code的解决方案,但range显然没有。

zhte4eai

zhte4eai1#

您可以使用Python的unittest.mock模块 Package builtins模块中的range函数,然后让测试Assert Package 后的range确实被调用了。
例如,使用Python的unittest框架编写测试:

import builtins
import unittest
from unittest.mock import patch

# I don't know what L is supposed to be, and I know that there 
# are better ways to compute average of a list, but the code
# for calculating the average is not important for this question.
def avg(L):
    total = 0
    for index in range(len(L)):
        total += L[index]
    return total / len(L)

class TestAverage(unittest.TestCase):
    def test_avg(self):
        with patch("builtins.range", wraps=builtins.range) as wrapped_patch:
            expected = 47
            actual = avg([1,49,91])
            self.assertEqual(expected, actual)
        wrapped_patch.assert_called()

if __name__ == '__main__':
    unittest.main()
$ python -m unittest -v main.py
test_avg (main.TestAverage) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

它使用unittest.mockpatch作为builtins.range函数的目标,通常情况下,patch用于替换目标的行为和/或返回值,但在本例中,您可以传递wraps=builtins.range(它被传递给底层的Mock对象),这意味着"* 我只想监视调用,但不修改其行为 *":

  • wraps *:要 Package 的模拟对象的项。如果 * wraps * 不是None,则调用模拟将把调用传递给 Package 的对象(返回实际结果)。

通过将其 Package 在Mock对象中,您可以使用Mock的任何assert函数来检查对range的调用,例如assert_called,它检查目标是否至少被调用过一次。您可以通过Assertrange被调用的次数来更具体地说明:

self.assertTrue(wrapped_patch.call_count == 1)

如果根本不调用Assert,则Assert将失败:
一个三个三个一个
在使用patch时最重要的是知道在哪里打补丁,在这种情况下,你可以查看文档或者使用__module__来知道range的模块:

>>> range
<class 'range'>
>>> range.__module__
'builtins'

但是这个测试有点幼稚,因为即使avg没有真正使用range,它仍然可以通过:

def avg(L):
    range(len(L))  # Called but really unused. Sneaky!
    return sum(L) / len(L)

class TestAverage(unittest.TestCase):
    # same as the code above
$ python -m unittest -v main.py
test_avg (main.TestAverage) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

一个稍微令人困惑的解决方案是"破坏" range的测试,这样,如果函数 * 真的 * 使用range,那么它将不再工作:

def avg(L):
    range(len(L))  # Called but really unused. Sneaky!
    return sum(L) / len(L)

class TestAverage(unittest.TestCase):
    def test_avg(self):
        # same as above

    def test_avg_is_really_using_range(self):
        L = [10,20,90]
        # Is it returning the correct result?
        self.assertEqual(avg(L), 40)

        # OK, but did it really use `range`?
        # Let's try breaking `range` so it always yields 0,
        # so we expect the return value to be *different*
        with patch("builtins.range", return_value=[0,0,0]):
            self.assertNotEqual(avg(L), 40)

因此,如果avg正在偷偷调用,但实际上并没有使用range,那么test_avg_is_really_using_range现在将失败,因为即使range已损坏,avg仍然会产生正确的值:

$ python -m unittest -v main.py
test_avg (main.TestAverage) ... ok
test_avg_really_using_range (main.TestAverage) ... FAIL

======================================================================
FAIL: test_avg_really_using_range (main.TestAverage)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/path/to/main.py", line 19, in test_avg_really_using_range
    self.assertNotEqual(avg(L), 40)
AssertionError: 40.0 == 40

最后,作为附带说明,我在所有示例中使用assertEqual,因为对返回值的测试不是重点,但请务必阅读Assert可能的浮点值的正确方法,例如How to perform unittest for floating point outputs? - python

相关问题