python 如何用 Hypothesis 来自动化单元测试

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

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

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

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

Hypothesis 的基础知识

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

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

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

Hypothesis 快速入门

1、安装

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

1

2

pip install hypothesis

pip install hypothesis[pandas,django]

2、使用

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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 (文件名随意),内容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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。现在来运行一下看看效果:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

(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 函数加入以下代码:

1

2

if not input_string:

 return []

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

1

2

3

4

@given(text())

def test_decode_inverts_encode(self, s):

 print(f"{s=}")

 self.assertEqual(decode(encode(s)), s)

再次执行:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

(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='?'

s='Î'

s='Î\U00085b9e'

s="Î8'?\U00057c38Ù;\x07\U000a5ea8Ò»=\U00091d5b~8뺈"

s='\U000d6497Ý>'

s='\U000e0f01'

s='\U000e0f01Å0y¢KN®'

s='\U000e0f01Å0y¢KN®'

s='\U00050a06'

s='Å\U000b98b3か\U000ba80aá`Ã-Êu\x8c\x90³FÔ"'

s='\x8e\U0004612a\x83ç'

s='\x8e'

s='\x8e\x98\U000fb3e0\U0010d2b3\x10\x82\x94Ð渥'

s='¥W'

s='p\U000e5a2aE·`ì'

s='\U000b80f8\x12\U000c2d54'

s='.\U000703de'

s='6\U00010ffa\U000f7994\x8e'

s='116\U000f7994\x8e'

s='1?6\U000f7994\x8e'

s='4?6\U000f7994\x8e'

s='4\x8e6\U000f7994\x8e'

s='0'

s='\U0006a564´Ð\x93ü\x9eb&i\x1cÑ'

s='\U000ceb6f'

s='\U000ceb6f\xa0\x08'

s='\U000ceb6f\xa0\x08'

s='\U000ceb6fꄃ\x08'

s='\U000ceb6fꄃ匀\U0007cc15\U000b2aaa×**'

s='\U000ceb6fꄃ匀'

s='匀ꄃ匀'

s='J\x14?ö'

s='q)'

s='q)'

s='q\U00060931'

s='q6'

s='\U000e3441'

s='\U000e3441\U00019958¯'

s='\x13'

s='\U000f34dbk'

s='Kp&tÛà'

s='\nö\x93'

s='\n\n\x93'

s='\U00019c8dѳ\U00056cbd\U000e3b2f\U00058d302'

s='\x90=R\x8bß\x03'

s='\x9a'

s='\U000147e7'

s='\U000147e7\x85\U0007a3ef'

s='\U000147e7\U00050a070Â>'

s='\U000a4089\x0eC+RÁ\x02\x97\x9cüÌïSS\U0006cbc5;ÿ~\x16\x019VÇ\U000a32fdQ÷\x15'

s='ÞÚ¾\x19©Z®'

s='ਸ਼æ'

s='\U000cd45a'

s='\U000cd45a\U000e15cbÑ\x08J\ueb3eúß\x07I\x91\x9a\x18\x16Ç\x80\x1a'

s='\x8f}º\x0eq\x0b'

s='\x0e}º\x0eq\x0b'

s="\U000e05a3&¶º[fõ\x8bÜR'ͼt\x97íW\x05\U000caea9\U0008fd74\U000e8f1c¹?dfƾ\x13"

s='\x10\U000e12e2ù\U0006f96erý\U00014baf\x00\x95\U000dbc92É\U00081613µ\U0003b865Z\U0008cc3c'

s='ú\U000b561f\x8fÎ'

s='\tàÖ÷'

s='à\x92©Ì\U000618fa\x92'

s='\U000aaf94\x94\x84\U000cda69\U0005291a\U000a63deþ¿O\x8a>\U000b458bÊ.\U00086f07\x1a'

s='\U0009754e?U_\xa0\x13PQ\x18º\x07\U0006c9c5.Á'

s='\U00102456'

s='³WᵎÕ'

s='\x14\x1c'

s='\x14'

s='\x14\U00105bcd"\x10Ô\x99\U000a5032R\U00056c44V&÷>+\U000aaff2ñ®\U000d7570%ª!\U00032553´8x^«'

s='\x00\U000e2ac4¼ÄUrB'

s='\x00\U000e2ac4¼ÄUrB'

s='\x00\U000e2ac4¼ÄUrB'

s='ª\x1aU\x8aÇ\U000b2fb9\U0005a586'

.

----------------------------------------------------------------------

Ran 1 test in 0.180s

OK

从执行结果可以看出,'' 首先被测试,其次 hypothesis 使用了大量的极端测试用例,减轻了手写的负担,大大提升了效率。

虽然 hypothesis 具有自动记忆功能,你仍然可以显式的指定某个测试用例一直被测试,而且这是推荐的做法,比如我想在每次的测试中都测试 '',可以这样写:

1

2

3

4

5

6

7

8

from hypothesis import given, example

from hypothesis.strategies import text

@given(text())

@example("")

def test_decode_inverts_encode(s):

 assert decode(encode(s)) == s

这一点非常有用,提升了测试代码的可读性,可以用来告诉开发人员或者未来的自己,输入的字符串必须要考虑 '' 的情形。

此外,执行单元测试,不一定要使用 unittest.main(),也可以这样,是不是很方便:

1

2

if __name__ == "__main__":

 test_decode_inverts_encode()

3、其他策略参考

从哪里开始

以上仅仅是抛砖引玉,hypothesis 还有很多自动化的特性,不再一一列举,最好的学习方法是边做,边尝试。

​现在我也找了很多测试的朋友,做了一个分享技术的交流群,共享了很多我们收集的技术文档和视频教程。
如果你不想再体验自学时找不到资源,没人解答问题,坚持几天便放弃的感受
qq群号:485187702【暗号:csdn11】
可以加入我们一起交流。而且还有很多在自动化,性能,安全,测试开发等等方面有一定建树的技术大牛
分享他们的经验,还会分享很多直播讲座和技术沙龙
可以免费学习!划重点!开源的!!!
视频+文档+PDF+面试题可以关注公众号:【软件测试小dao】

最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走! 希望能帮助到你!【100%无套路免费领取】

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值