赞美基于财产的检测(2019年)

2021-04-03 21:03:39

基于物业的测试是一种源自Haskell Library QuickCheck的测试风格。自2015年初以来,我一直在努力将其纳入主流,当时我发布了Python库假说,这已经被视为相当广泛的采用。我想告诉你一些基于物业的测试以及它为什么要这样做

传统或基于示例,测试指定软件的行为通过写入IET-each测试设置单个具体方案并断言软件在该方案中的表现方式。基于物业的测试采取这些具体场景,并通过专注于哪些方案的特征至关重要,并且允许变化。这导致更清晰的测试,更好地指定软件的行为 - 以及传统测试错过的更好的未发现错误

基于示例的测试的问题是他们最终使得远远超强的索赔,其实际能够证明。基于物业的测试通过表达预期我们的测试的情况完全相同,提高了这一点。基于示例的测试使用具体方案来建议对系统行为的一般索赔,而基于物业的测试直接关注该一般索赔。同时,基于物业的测试库提供了测试索赔的工具

要了解焦点工作的转变如何,请查看一个相当典型的基于示例的测试。假设我们正在测试一个Web应用程序,允许用户在项目上协作。项目有最多的合作者数量,我们希望能够将用户添加到该限制。要验证我们可以执行此操作,我们编写以下测试:

从.models导入用户,从django.test导入testcase类(testcase):def(self):project = project.objects.create(collaborator_limit = 3,name ="一些项目")alex =用户.Objects.Create(电子邮件=" [email protected]")Kim = User.Objects.Create(Email =" [email protected]")pat = user.objects.create (电子邮件=" [email protected]")project.add_user(alex)project.add_user(kim)project.add_user(pat)self.asserttrue(project.team_contains(axx))self.asserttrue(项目.team_contains(kim))self.asserttrue(project.team_contains(pat))

为了测试我们的一般索赔(我们可以将用户添加到项目的协作限制),我们已经编写了一般索赔的特定实例的测试。这不是一个不合理的事情:如果这项测试失败,我们的索赔肯定是假的。问题是,如果我们的测试通行证,它并没有告诉我们索赔本身 - 只是告诉我们测试通过。这在测试名称中不会很好地反映。标题测试_ CAN_ ADD_用户_ UP___COLLABORATOR_限制“确定声音,如一般索赔。一个更准确的名字是test_can_ add_ users_at_aple_ domain_ to_ a_ project_用_ aga and _ a_ collaborator_ limit_ _3“ - 但这不太可能是一个特别流行的命名约定

这是基于示例性测试的根本问题:我们经常将我们的测试视为规范,但实际上他们是故事。更糟糕的是,他们常常毛茸茸的狗的故事,充满了一个乱糟糟的随机细节,我们没有任何线索,就像哪些部分的测试实际上很重要,哪些部分只是一个分心

看看上一页的测试。哪些细节有关?可能是 - 很快! - 项目名称是无关紧要的。合作镜限制是否特别是三个重要?可能不是,但它可能重要的是,它大于一个。用户所有人都有同一域的电子邮件地址是否重要?也许,但测试并没有说

基于物业的测试是关于删除那些无关的细节,而基于物业的测试库是帮助我们这样做的工具

从.models导入用户,从假设导入中的假设导入从假设导入中导入测试..extra.django.models从假设导入模型.strategies导入文本,列出类(testcase):text()文本()列表( Unique_by = lambda u:u.email))def(self,project_name,协作者):project = project.create(name = project_name,collaborator_limit = len(collaborators)在协作者中为c:project.add_user(c)对于COLCRABORATORS中的C:self.asserttrue(project.team_contains(c))

这是相同的测试,但我们不确定的详细信息现在允许不同:而不是固定的项目名称或用户集,我们已经表示,这适用于任何项目名称和任何不同用户列表。该测试现在捕获完全是我们的原始意图,因为我们通过允许它们不同,我们抽象了不重要的细节

这项工作的方式是假设使用@given装饰器让我们为测试指定一系列有效输入。这些输入使用策略指定,该策略描述了要采取的参数的有效值范围

在这种情况下,我们的项目名称可能是任何字符串,我们的协作者可以是任何用户模型对象列表,只要它们都具有不同的电子邮件地址。预计以这种方式编写的测试将通过其策略允许的任何可能的论点

我们运行此新版本的测试时会发生什么?好吧,它通过了。它需要一个相当钝的实现来失败原始测试并通过这一个。在这里,原始测试的改进纯粹是在使这一更好的考试方面,但是测试套件的质量已经大幅增加

人们使用夹具和工厂库是常见的,以减少一遍又一遍地建立数据的乏味。这导致测试依赖于(以微妙和无意的方式)对夹具数据的细节,并且由于结果而变得越来越脆弱。基于物业的测试避免了坚持不懈地允许无关紧要的细节来避免脆性,这使得测试不可能取决于它们。结果是一个显着的清洁和更强大的测试套件,这使得夹具数据的隐式假设较少

它比这更重要!通过迫使我们精确描述我们的软件的行为,基于物业的测试依次迫使我们显明不仅仅是我们在编写测试时所做的假设,而且是我们在编写软件时所做的假设。我们常会发现这些假设是错误的

让我们来看看这种错误假设的两个例子,从假设发现的真实世界(但旧的)错误

我们为Python绑定写了一些测试到argon2,屡获殊荣的密码散列库。测试本身相当简单(拍摄密码,散列,验证哈希的原始密码),但有趣的功能是argon2需要很多配置选项来控制难度,并且通过允许这些允许这些选项允许我们曝光的难度(基础实施中的错误)错误

从argon2导入passwordhasher从假设导入给定,假设导入假设.strategies作为st类(对象):password = st.text(),time_cost = st.integers(1,10),parturantism = st.integers(1,10) ,memory_cost = st.integers(8,2048),hash_len = st.integers(12,1000),star_len = st.integers(8,1000))def(self,password,time_cost,parturentom,memory_cost,hash_len,salt_len) :#拒绝具有每线程少于8的内存成本的示例,如无效#(在构建密码哈赫时会引发错误)假设(并行性* 8< = memory_cost)ph = passwordhasher(time_cost = time_cost,parallelism = parallyism ,memory_cost = memory_cost,hash_len = hash_len,salt_len = salt_len)hash = ph.hash(密码)断言ph.verify(哈希,密码)

这里测试确实失败了。发生这种情况时,假设打印了触发该故障的参数的特定组合:

错误是,如果散列长度大于512,则它会在底层C库中击中内部固定大小缓冲区,这会导致验证出错。默默地似乎有效,但结果哈希不会验证原始密码

我们是如何获得此输出的?这是基于物业的测试库,在这种情况下的假设中进来。当我们在假设中进行测试时,这就是发生的事情:

如果它有一个失败的示例,它会缩小缩小以尝试找到触发相同错误的更简单的参数集。

特别是缩小,是使用基于物业的测试库与随机生成的夹具库上的大益处之一。调试随机生成的值让我们回到必须询问细节的情况。随机生成的值通常比人类写在它们的那样差,因为它们很大而凌乱 - 这使得很难挑选出什么事

相比之下,考虑上一页的伪造示例:所有参数都是它们可以是-Expt的最小值 - 它是它可能在仍然触发错误时的最小值。如果我们用512更换513,则会消失。当我们使用这些缩放示例时,负责失败的细节倾向于脱颖而出

这是基于属性的测试有助于发现的第一种假设:关于什么类型的输入调用某些功能的假设。基于物业的测试要求我们明确关于有效范围,这有助于我们找到它所实际的东西,而不是测试快乐的路径

另一个错误的假设来源是当软件的部分是由不同的人写的,因为我们使用第三方库或仅仅因为团队中有多个人。当假设隐含时,当不同的人制作不同的人时,很难注意到

以下是这种不匹配的一个有趣的例子。这来自一个名为binaryornot的库,其工作是启发式地检测文件是否符合二进制文件或文本文件:

来自Binaryornot.helpers从假设给出的假设导入中导入IS_Binary_String.strategiate导入二进制Def(s):is_binary_string(s)

在这里,我们生成了一个字节字符串来测试并将其传递给IS_Binary_String函数。请注意,我们甚至没有检查是否有任何明智的情况!我们只是检查功能是否提出了错误。这通常是与基于物业的测试写入的非常有用的测试,因为它很容易实现并且经常刷出令人惊讶的错误数量。使用断言或合同,我们甚至可以通过确保我们的代码在可能默默地失败的情况下崩溃,进一步提高它

在这种情况下,测试失败了输入S = B' \ xae \ xc5 \ xdc' m,由Unicode解码误差引起。为什么?好吧,因为它依赖于另一个名叫Chardet的图书馆。 Chardet是否有启发式预测文本文件的预期编码。在这种情况下,逻辑是BinaryOrnot认为这个字符串可能是文本,要求Chardet预测其编码,并被告知它是一个特定的编码,达到100%的信心。不幸的是,事实证明,当Chardet说没有预期的含义时,这实际上意味着它是该编码的有效字节序列

这是记录的行为,并且具有足够合理的动力,但它也很令人惊讶的是,Binaryornot作者(同样合理地)从未考虑过这种可能性。合同之间存在不匹配,他们相信Chardet遵循以及它实际遵循的合同,但在通过基于物业的测试测试的代码测试之前,这种不匹配不会出现

我们现在到达大多数基于物业的测试文章开始的地方:只有在基于属性的属性时,只能真正有意义的测试。由于基于物业的测试使得易于编写在广泛参数范围内的测试,因此它会提示我们思考我们可以使我们能够使其概括的程序。这些是基于物业测试中的“属性”。

考虑这些属性可能很难,所以我通常建议人们对他们太担心,直到他们熟悉基于物业的测试基础并将其集成到正常的工作流程中。但是,有一个容易而普遍的是,从一开始就值得了解:编码/解码,或圆跳闸,测试

当我们有一些数据我们转换为序列化表示时,我们可以始终检查序列化IT并反序列化它产生相同的结果。这是有用的两个原因:首先,基本上每个非竞争应用程序都将其状态序列化到数据库中,进入API。其次,这种序列化通常具有错误,结果,当我们从一种格式转换为另一个格式时,重要信息会丢失或损坏

从dateutil.parser导入解析从假设导入给定,假设的设置.strategies导入datetimes def(dt):formatted = dt.isoformat()断言格式== parse(格式化).isoformat()

这测试了Dateutil库在ISO 8601格式中解析时间(一种真实日期格式!)如下:

然后假设检查此日期与原件同意。 (我们检查它们具有相同的ISO 8601格式而不是由于时区对象的平等复杂而直接平等。)

当给定0005-01- 01T00:00:05时,此功能失败,它错误地解析为0001-05-01T00:00:05,交换年份和月份

此错误非常具体。它只发生在这一年等于第二年,并且它发生在解析器如何解释日期的一些歧义。它非常不太可能被正常,人类测试过程中发现,但有些用户最终会遇到它。希望他们本来已经注意到,而不是让他们的数据默默地腐败。通过编写基于属性的测试,在不同格式之间转换数据时,可以断言数据的一致性,我们消除了从生产中的全类微妙错误

桥接我们主张测试的差距以及我们实际测试的内容。

揭示我们在测试和开发期间所做的假设,并检查它们是否被侵犯。

在我们的代码中公开微妙的不一致,这将难以使用基于示例的测试来检测。

但是,如果您从基于示例的测试套件开始,您将如何到达那里?最重要的是刚刚开始。基于大多数基于物业的测试库都是为常用测试框架而轻松集成的,因此添加到现有的连续集成相当容易。您可以开始小,使一些基于示例的实例的测试进入简单的属性,将它们作为网关,以便使用基于物业的测试的初始障碍

在测试套件中有几个基于物业的测试后,您可以在正常开发过程中添加新的基于物业的测试。该测试可以是基于物业的测试吗?“在代码审查期间询问是一个很好的问题。并触摸现有代码是在编写新功能之前概括测试的好机会

或者,如果您想通过在深端跳跃开始时,可​​以休闲休闲,让整个团队在一起努力添加基于物业的测试。这将进一步得多,但被警告:你可能会发现很多虫子!