用 Hypothesis 来自动化单元测试

高质量的代码离不开单元测试,而设计单元测试的用例往往又比较耗时,而且难以想到一些极端情况,本文讲述如何使用 Hypothesis 来自动化单元测试

刷过力扣算法题的同学都知道,有时候觉得代码已经很完善了,一提交才发现很多情况没有考虑到。然后感叹力扣的单元测试真的牛比。

因此,高质量的代码离不开单元测试,如果现在还没有写过单元测试,建议先去学习以下常用的单元测试库[1],只要实践过,才能感受到本文开头提到的那些痛点。

Hypothesis 是一个 Python 库,用于让单元测试编写起来更简单,运行时功能更强大,可以在代码中查找您不会想到的极端情况。它稳定,强大且易于添加到任何现有测试框架中。它的工作原理是让您编写断言每种情况都应该正确的测试,而不仅仅是您偶然想到的那些。

Hypothesis 的基础知识

典型的单元测试需要自己写一些测试用例,然后编写测试函数,通过一段代码运行它,然后根据预期结果检查结果。

Hypothesis 有所不同。它是基于属性进行单元测试。它通过生成与您的规范匹配的任意数据并检查在这种情况下程序是否仍然有效。如果找到了一个失败的用例,它将采用该示例并将其测试用例范围缩减缩减为一定尺寸,然后对其进行简化,直到找到一个仍会导致问题的小得多的示例。然后将其保存,后续单元测试时仍会使用这些用例。

现在就让我们看看怎么用吧。

Hypothesis 快速入门

1、安装

可以通过 pip 安装,也可以通过源代码安装[2],也可以安装一些扩展[3],如下:

pip install hypothesis
pip install hypothesis[pandas,django]

2、使用

先写一段代码,保存在 mycode.py 中,功能是对字符串进行特定的编码和解码,内容如下:

def encode(input_string):
    count = 1
    prev = ""
    lst = []
    for character in input_string:
        if character != prev:
            if prev:
                entry = (prev, count)
                lst.append(entry)
            count = 1
            prev = character
        else:
            count += 1
    entry = (character, count)
    lst.append(entry)
    return lst


def decode(lst):
    q = ""
    for character, count in lst:
        q += character * count
    return q

对这段代码进行单元测试,往往需要写很多测试用例,现在我们使用 hypothesis 来自动为我们测试,编写 test_mycode.py (文件名随意),内容如下:

from hypothesis import given
from mycode import decode,encode
from hypothesis.strategies import text
import unittest


class TestEncoding(unittest.TestCase):
    @given(text())
    def test_decode_inverts_encode(self, s):
        self.assertEqual(decode(encode(s)), s)


if __name__ == "__main__":
    unittest.main()

可以看出,这里并没有出现具体的测试用例,而是使用来 text 的策略,相当于 hypothesis 自动穷举来可能的情况,也可以看出它很容易可其他测试框架集成,这里是 unittest。现在来运行一下看看效果:

(py38env) ➜  tmp python test_mycode.py
Falsifying example: test_decode_inverts_encode(
    self=<__main__.TestEncoding testMethod=test_decode_inverts_encode>, s='',
)
E
======================================================================
ERROR: test_decode_inverts_encode (__main__.TestEncoding)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_mycode.py", line 9, in test_decode_inverts_encode
    def test_decode_inverts_encode(self, s):
  File "/Users/aaron/py38env/lib/python3.8/site-packages/hypothesis/core.py", line 1162, in wrapped_test
    raise the_error_hypothesis_found
  File "test_mycode.py", line 10, in test_decode_inverts_encode
    self.assertEqual(decode(encode(s)), s)
  File "/Users/aaron/tmp/mycode.py", line 14, in encode
    entry = (character, count)
UnboundLocalError: local variable 'character' referenced before assignment

----------------------------------------------------------------------
Ran 1 test in 0.048s

FAILED (errors=1)

这里测试出当字符串为 '' 的时候会抛出 UnboundLocalError 的异常。现在我们来修复这个 bug,然后把所有的测试用例 s 给打印出来,看看它用了哪些测试用例。

encode 函数加入以下代码:

if not input_string:
    return []

test_mycode.py 文件打印出测试用例:

@given(text())
def test_decode_inverts_encode(self, s):
    print(f"{s=}")
    self.assertEqual(decode(encode(s)), s)

再次执行:

(py38env) ➜  tmp python test_mycode.py
s=''
s='1'
s='0'
s='0'
s='0'
s='Ā'
s='\U000cf5e5'
s='0'
s=''
s='0'
s='0'
s='E'
s=")dù'\x18\U0003deb3¤jd"
s='\U0005bc37\x07\U000537a1ÝÀãiÎ\U000ce9e5\x0b'
s='\U0005bc37\U0005bc37\U000537a1ÝÀãiÎ\U000ce9e5\x0b'
s='\U0005bc37\U000537a1\U000537a1ÝÀãiÎ\U000ce9e5\x0b'
s='À\U000537a1\U000537a1ÝÀãiÎ\U000ce9e5\x0b'
s='\U000965e1\x12\x85&\U000f500aÄÃc'
s='\n\U0004466c\x86Î\x07'
s='Ê\U00063f1e\x01G\x88'
s='ÚV\n'
s='VV\n'
s='\U0008debf湆è'
s='\U0008debf湆è'
s='\U0008debf湆'
s='\U0008debf\U0008debf'
s='\U0008debf\U0008debfó]½àq\x82#\U00015196\U0001c8beg'
s='\U0008debfgó]½àq\x82#\U00015196\U0001c8beg'
s='
上一篇:PYTHON奇幻冒险记23 走进编程的世界!


下一篇:python自动生成公私钥