下载安卓APP箭头
箭头给我发消息

客服QQ:3315713922
论坛 >编程语言 >提高你的Python能力:理解单元测试 (上)

提高你的Python能力:理解单元测试 (上)

希尔瓦娜斯发布于 2018-01-17 10:14查看:871回复:1

对于程序开发新手来说,一个最常见的困惑是测试的主题。他们隐约觉得“单元测试”是很好的,而且他们也应该做单元测试。但他们却不懂这个词的真正含义。如果这听起来像是在说你,不要怕!在这篇文章中,我将介绍什么是单元测试,为什么它有用,以及如何对Python的代码进行单元测试。

什么是测试?

在讨论为什么测试很有用、怎样进行测试之前,让我们先花几分钟来定义一下“单元测试”究竟是什么。在一般的编程术语中,“测试”指的是通过编写可以调用的代码(独立于你实际应用程序的代码)来帮助你确定程序中是否有错误。这并不能证明你的代码是正确的(在非常有限的情况下这是唯一的可能)。它只是报告了测试者认为的那种情况是否被正确处理了。

注:当我使用“测试”一次时,我指的是“自动化测试”,即这些测试是在机器上运行的。“手动测试”则是一个人运行程序,并与它进行交互,从而发现漏洞,这是个独立的概念。

测试可以检查出什么样的情况呢?语法错误是语言的意外误用,如

image.png

后面多余的一个 “.“。逻辑错误是当算法(可以看成是“解决问题的方式”)不正确时引发的。可能程序员忘记Python是“零索引“的并且试图通过写

image.png

(这样会引起IndexError)来打印出一个字符串中的最后一个字符。更大、更系统的错误也可以被检查出来。比如当用户输入一个大于100的数字、或者在网站检索不可用的时候挂起此网站的话,程序会一直崩溃。

这些所有的错误都可以通过对代码的仔细测试检查出来。Unit testing,特指在一个分隔的代码单元中的测试。一个单元可以是整个模块,一个单独的类或者函数,或者这两者间的任何代码。然而,重要的是,测试代码要与我们没有测试到的其他代码相互隔离(因为其它代码本身有错误的话会因此混淆测试结果)。考虑如下例子:

image.png

你有两个函数,is_primeprint_next_prime。如果你想测试print_next_prime,我们就需要确定is_prime是正确的,因为print_next_prime中调用了这个函数。在这种情况下,print_next_prime函数是一个单元,is_prime函数是另一个单元。由于单元测试每次只测试一个单元,因此我们需要仔细考虑怎样才能准确的测试print_next_prime?(更多的是关于之后怎样实现这些测试)。

因此,测试代码应该长什么样呢?如果上一个例子存在一个叫primes.py的文件中,我们可以把测试代码写在一个叫test_primes.py的文件中。下面是test_primes.py 中的最基本内容,比如下面这个测试样例:

image.png

这个文件通过一个test case:?test_is_five_prime创建了一个单元测试。通过Python内嵌的一个测试框架unittest。当unittest.main()被调用时,任何一个以test开头命名的成员函数将被运行,他们是unittest.TestCase的一个派生类,并且是断言检查的。如果我们通过输入python test_primes.py来运行测试,我们能够看到unittest框架在控制台上 的输出:

image.png


单独的“E”表示的是我们单元测试的结果(如果它成功了,会打印出一个“.”)。我们可以看到我们的测试失败了,以及导致失败的那行代码,还有任何引发的异常信息。

为什么要测试?

在我们继续那个例子之前,要问个很重要的问题:“为什么测试对我来说有价值”?这是个公平的问题,也是那些对于代码测试不熟悉的人常问的问题。毕竟,测试需要一定的时间,而我们完全可以用这些时间去编代码,为什么要测试而不是去做那些最有生产效率的事?

有很多答案可以有效的回答这个问题,我列出了以下几点:

  • 测试可以保证你的代码在一系列给定条件下正常工作

测试确保了一系列条件下的正确性。语法错误基本上一定通过测试被查出来,一个代码单元的基本的逻辑也可以通过测试被检测出来,以确保一定条件下的正确性。再次,它不是要证明代码是在任何条件下都正确的。我们只是简单的瞄准了一套比较完整的可能的条件(例如,你可以写一个测试来监测当你调用my_addition_function(3, 'refrigerator), 的时候,但你不必为每个参数检测所有可能的字符串)

  • 测试允许人们确保对代码的改动不会破坏现有的功能

重构代码时,这一点特别有用。如果没有测试到位,你就没法保证你的代码的改变没有破坏之前工作正常的东西。如果你希望更改或重写你的代码,并希望不会破坏任何东西,适当的单元测试是很必要的。

  • 测试迫使人们在不寻常条件的情况下思考代码,这可能会揭示出逻辑错误

编写测试强迫你去思考在非正常条件下你的代码可能遇到的问题。在上面的例子中,my_addition_function函数可以将两个数字相加。测试基本正确性的简单测试将调用my_addition_function(2,2),并断言说结果是4。然而,进一步的测试可能会通过调用my_addition_function(2.0,2.0)来测试该功能是否能正确进行浮点数的运算。防御性的编码原则表明你的代码应该能够在非法输入的情况下正常失效,因此测试时,当字符串类型被作为参数传递到函数中时应当抛出一个异常。

  • 良好的测试要求模块化,解耦代码,这是一个良好的系统设计的标志

单元测试的整体做法是通过代码的松散耦合使其变得更容易。如果你的应用程序代码直接调用数据库,例如,测试你应用程序的逻辑依赖于一个有效的数据库连接,并且测试数据要存在于数据库中。另一方面,隔离了外部资源的代码在测试过程中更容易被模拟对象所替代。出于必要,(人们)设计的有测试能力的应用程序最终采用了模块化和松散耦合。

单元测试的剖析

通过继续之前的例子,我们将看到如何编写并组织单元测试。回想一下,primes.py包含以下代码:

image.png

同时,文件test_primes.py包含如下代码:

image.png


做出断言

unittest是Python标准库中的一部分,并且也是我们开始“单元测试之旅”的一个好的起点。一个单元测试中包括一个或多个断言(一些声明被测试代码的一些属性为真的语句)。会想你上学的时候“断言”这个词的字面意思就是“陈述事实”。在单元测试中,断言也是同样的作用。

self.assertTrue 更像是自我解释。它能声明传递过去的参数的计算结果为真。unittest.TestCase类包含了许多断言方法,所以一定要检查列表并选择合适的方法进行测试。如果在每个测试中都用到assertTrue的话,则应该考虑一个反模式,因为它增加了测试中读者的认知负担。正确使用断言的方法应当是使测试能够明确说明究竟是什么在被断言(例如,很明显?,只需扫一眼assertIsInstance 的方法名,就知道它要说明的是其参数)。

每个测试应该测试一个单独、有具体特性的代码,并且应该被赋予相关的命名。就单元测试发现机制的研究表明(主要在Python2.7+和3.2+版本中),测试方法应该以test_为前缀命名。(这是可配置的,但是其目的是鉴别测试方法和非测试的实用方法)。如果我们把test_is_five_prime 的命名改为is_five_prime的话,运行python中的test_primes.py时会输出如下信息:

image.png


不要被上面信息中的“OK”所糊弄了,只有当什么测试都没真正运行的时候才会显示出“OK”!我认为一个测试也没跑其实应该显示个报错的,但是个人感觉放在一边,这是一个你应该注意是行为,尤其是当通过程序运行来检查测试结果的时候(例如,一个持续的集成工具,像TracisCI)。

异常

让我们回到test_primes.py的实际内容中去,回忆一下运行python test_primes.py指令后的输出结果:

image.png


这些输出告诉我们,我们一个测试的结果失败并不是因为一个断言失败了,而是因为出现了一个未捕获的异常。事实上,由于抛出了一个异常,unittest框架并没有能够运行我们的测试就返回了。

这里的问题很明确:我们使用的求模运算的计算范围中包括了0,因此执行了一个除以0的操作。为了解决这个问题,我们可以很简单的将起始值由0变为2,并指出对0求模是错误的,而对1求模则一直是真(并且一个素数只能被自身和1整除,因此我们无需检查1)。



收藏(0)0
查看评分情况

全部评分

此主贴暂时没有点赞评分

总计:0

回复分享
希尔瓦娜斯  于   2018-01-17 10:30 重新编辑过

版主推荐

    共有1条评论

    • IT宅男
    • mr jack
    • Mr ken
    • Mright
    • cappuccino
    • YUI
    • 课课家运营团队
    • 课课家技术团队1
    • 酸酸~甜甜
    • 选择版块:

    • 标题:

    • 内容

    • 验证码:

    • 标题:

    • 内容

    • 选择版块:

    移动帖子x

    移动到: