自我编写的单元测试


Python部落组织翻译,禁止转载,欢迎转发。

单元测试通常最简单的形式就是往黑盒(可能是一个函数或很多步操作)中输入一些数值,然后检查输出的结果是否与预期相匹配。

这是很好,使得单元测试变得非常的明确,简单,可重用。不幸的是有一些缺陷,最明显的就是你相信将每一个单元测试的结果聚集起来就会覆盖到所有路径,当我谈到一条路径时,我是指一个函数的一条独特的执行流。

Hypotheis是一个了不起的库,由Haskell的QuickCheck包启发而成的Python中非常让人难以置信的工具。

本质上它是用来产生测试数据的,但是它会注意到内存泄露以及越界等会使测试崩溃的问题,因而,在某种程度上它可以为你编写单元测试。

本篇中剩下的部分就是取自它们提供的例子。

假设我们已经编写了一个“run length encoding”系统,并且想要对它进行测试。

实际代码如下,它来自于Rosetta Code wiki,我只是将其中的一些注释去除并纠正下格式,并没有任何功能性改动。

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

else:

entry = (character, count)

lst.append(entry)

return lst

def decode(lst):

q = ""

for character, count in lst:

q += character * count

return q

现在来对它进行测试,并针对这些函数进行检查。

人们可能想做的测试就是看encoding/decoding函数是否真的能正确地将输入内容进行编码和解码。

让我们看下如何使用Hypothesis来完成这些:

from hypothesis import given

from hypothesis.strategies import text

@given(text)

def test_decode_inverts_encode(s):

assert decode(encode(s)) == s

在这个例子中我们使用到pytest进行测试,在之后的例子中我们将会尝试其他方法。

Text函数返回了Hypothesis的一种搜索策略,即一种方法对象能够描述如何获取并简化特定类型的值。接着@given装饰器会包装我们的测试函数并将其参数化,当被调用时,会运行测试函数并根据策略对大范围匹配数据进行测试。

很快测试结果发现代码中存在一个BUG。

Falsifying example: test_decode_inverts_encode(s="")

UnboundLocalError: local variable "character" referenced before assignment

Hypothesis正确地指出当输入空的字符串时代码就会出错。

当我们在函数开始处添加如下代码时,Hypothesis会显示代码正确。

if not input_string:

return

如果我们想让Hypothesis永远检测到特定的输入可以显示地添加进去:

from hypothesis import given, example

from hypothesis.strategies import text

@given(text)

@example("")

def test_decode_inverts_encode(s):

assert decode(encode(s)) == s

你不必做这些,但是为了明确地表示你的目的并且让测试触及到那些很难找到的例子,做这些是值得的。如果是本地测试,Hypothesis会记住之前的测试用例并重复使用,但是目前还没有很好的共享方法。

当然如下代码也是对的:

@given(s=text)

@example(s="")

def test_decode_inverts_encode(s):

assert decode(encode(s)) == s

假设你还有更有意思的BUG——忘了每次重置count的值,即我们在encode函数中缺少了一行:

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 # Missing reset operation

prev = character

else:

count += 1

else:

entry = (character, count)

lst.append(entry)

return lst

Hypothesis会通知你如下错误:

Falsifying example: test_decode_inverts_encode(s="001")

注意到提供的例子非常的简单,Hypothesis不仅只是找到反例,它还会将得到的结果进行简化以利于理解。在这个例子中,两个相同的数值已经可以使得count的值发生改变,而后跟一个不同的值会导致count重置但是本例中并没有。

Hypothesis提供的例子都是可以直接运行的Python代码,那些你显式提供给函数的参数Hypothesis是不会再产生测试数据的,如果你已经显式提供了所有的参数,那么Hypothesis仅仅会运行一次而不是多次。

英文原文:http://elliot.land/unit-tests-that-write-themselves-hypothesis-python

译者:lovekk