简体   繁体   English

如何测试 function 呼叫范围是否在 python 内?

[英]How to test if a function calls range in python?

I'm a Python instructor and I wanted to give my students a task: write a function that computes the average of a list using a for loop and the range object.我是 Python 讲师,我想给我的学生一个任务:编写一个 function,使用 for 循环计算列表的平均值,范围为 object。

I wanted to run a test on their function to see whether it actually uses the range object. How can I do that?我想对他们的 function 进行测试,看看它是否实际使用范围 object。我该怎么做?

It should be something like this:它应该是这样的:

def avg(L):
    Pass

def test_range(avg):
    ...

If avg contains range , then test_range should return True .如果avg包含range ,那么test_range应该返回True

I tried solutions that utilize func_code , but apparantly range doesn't have that.我尝试了利用func_code的解决方案,但显然range没有。

You can use Python's unittest.mock module to wrap around the range function from the builtins module, and then let your test assert that the wrapped range was indeed called.您可以使用 Python 的unittest.mock模块从builtins模块环绕range function,然后让您的测试断言环绕的range确实被调用了。

For example, using Python's unittest framework for writing the test:例如,使用 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

It uses unittest.mock 's patch targetting the builtins.range function. Normally, patch is used in tests to replace the behavior and/or return value of the target, but in this case, you can pass wraps=builtins.range (which gets passed to the underlying Mock object), which means " I just want to spy on the call, but not modify its behavior ":它使用unittest.mock的针对 builtins.range builtins.rangepatch 。通常, patch用于测试以替换目标的行为和/或返回值,但在这种情况下,您可以传递wraps=builtins.range (这被传递给底层的Mock对象),这意味着“我只想监视调用,但不想修改它的行为”:

wraps : Item for the mock object to wrap. wraps :用于模拟 object 包装的项目。 If wraps is not None then calling the Mock will pass the call through to the wrapped object (returning the real result).如果wraps不是None那么调用 Mock 会将调用传递给包装的 object(返回真实结果)。

By wrapping it in a Mock object, you can use any of Mock 's assert functions to check calls to range , like assert_called which checks whether the target was called at least once.通过将其包装在Mock object 中,您可以使用Mock的任何断言函数来检查对range的调用,例如assert_called检查目标是否至少被调用一次。 You can be more specific by asserting the number of times range was called:您可以通过断言调用range的次数来更具体:

self.assertTrue(wrapped_patch.call_count == 1)

The assertion would fail if it wasn't called at all:如果根本不调用断言,断言就会失败:

# 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.

The most important thing to note when using patch , is to know exactly where to patch .使用patch时要注意的最重要的事情是要确切地知道在哪里打补丁 In this case, you can check the docs or use __module__ to know range 's module:在这种情况下,您可以查看文档或使用__module__来了解range的模块:

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

But the test is a bit naive, because it can still pass even though avg didn't really use range :但是测试有点幼稚,因为即使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

A slightly confusing workaround would be a test that "breaks" range such that, if the function was really using range , then it wouldn't work anymore:一个稍微令人困惑的解决方法是“打破” range的测试,如果 function真的使用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)

So, if avg was sneakily calling but not really using range , then test_avg_is_really_using_range would now fail because avg still yields the correct value even with a broken range :因此,如果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

Lastly, as a side note, I'm using assertEqual here in all the example because the test for the return value is not the focus, but do read up on proper ways to assert possible float values, ex.最后,作为旁注,我在所有示例中都使用了assertEqual ,因为返回值的测试不是重点,但请务必阅读断言可能的浮点值的正确方法,例如。 How to perform unittest for floating point outputs?如何对浮点输出执行单元测试? - python - python

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM