【问题标题】:How to test if a function calls range in python?如何测试函数是否在python中调用范围?
【发布时间】:2023-01-28 15:02:14
【问题描述】:

我是一名 Python 讲师,我想给我的学生一个任务:编写一个函数,使用 for 循环和范围对象来计算列表的平均值。

我想对他们的功能进行测试,看看它是否真的使用了范围对象。我怎样才能做到这一点?

它应该是这样的:

def avg(L):
    Pass

def test_range(avg):
    ...

如果avg包含range,那么test_range应该返回True

我尝试了使用func_code 的解决方案,但显然range 没有。

【问题讨论】:

    标签: python python-3.x unit-testing range


    【解决方案1】:

    您可以使用 Python 的 unittest.mock 模块来包装 builtins 模块中的 range 函数,然后让您的测试断言包装的 range 确实被调用了。

    例如,使用 Python 的 unittest 框架编写测试:

    import builtins
    import unittest
    from unittest.mock import patch
    
    # I don't know what L is supposed to be, nor do I know what 
    # avg is expected to do, 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 = 3
                actual = avg([1,2,3,4,5])
                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 对象),这意味着“我只是想监视调用,但不修改其行为”:

    包裹: 要包装的模拟对象的项目。如果包裹不是None 那么调用 Mock 会将调用传递给包装对象(返回真实结果)。

    通过将其包装在 Mock 对象中,您可以使用任何 Mock 的断言函数来检查对 range 的调用,例如 assert_called 检查目标是否至少被调用一次。

    如果根本不调用断言,断言就会失败:

    # Here, `range` wasn't used at all.
    def avg(L):
        return sum(L) / len(L)
    
    class TestAverage(unittest.TestCase):
        # same as the code above
    
    $ python -m unittest -v main.py
    test_avg (main.TestAverage) ... FAIL
    
    ======================================================================
    FAIL: test_avg (main.TestAverage)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/path/to/main.py", line 15, in test_avg
        wrapped_patch.assert_called()
      File "/usr/local/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/unittest/mock.py", line 888, in assert_called
        raise AssertionError(msg)
    AssertionError: Expected 'range' to have been called.
    

    使用patch时最重要的是要准确知道where to 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 损坏,它仍然会产生正确的值:

    $ 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,因为返回值的测试不是重点,但请务必阅读断言可能的浮点值的正确方法,例如。 How to perform unittest for floating point outputs? - python

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-09-18
      • 1970-01-01
      • 2017-07-22
      • 2021-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-07
      相关资源
      最近更新 更多