首頁  >  文章  >  後端開發  >  提升你的Python能力:理解單元測試

提升你的Python能力:理解單元測試

高洛峰
高洛峰原創
2016-10-17 16:49:231151瀏覽

對於程式開發新手來說,一個最常見的困惑是測試的主題。他們隱約覺得「單元測試」是很好的,而且他們也應該做單元測試。但他們卻不懂這個字的真正意義。如果這聽起來像是在說你,不要害怕!在這篇文章中,我將介紹什麼是單元測試,為什麼它有用,以及如何對Python的程式碼進行單元測試。

什麼是測試?

在討論為什麼測驗很有用、怎樣進行測試之前,讓我們先花幾分鐘來定義一下「單元測試」究竟是什麼。在一般的程式設計術語中,「測試」指的是透過編寫可以呼叫的程式碼(獨立於你實際應用程式的程式碼)來幫助你確定程式中是否有錯誤。這並不能證明你的程式碼是正確的(在非常有限的情況下這是唯一的可能性)。它只是報告了測試者認為的那種情況是否被正確處理了。

註:當我使用“測試”一次時,我指的是“自動化測試”,即這些測試是在機器上運行的。 「手動測試」則是一個人運行程序,並與它進行交互,從而發現漏洞,這是一個獨立的概念。

測試可以檢查出什麼樣的情況呢?文法錯誤是語言的意外誤用,如

my_list..append(foo)

   

後面多餘的一個 “.”。邏輯錯誤是當演算法(可以看成是「解決問題的方式」)不正確時引發的。可能程式設計師忘記Python是「零索引「的並且試圖透過寫

print(my_string[len(my_string)])

   

(這樣會引起IndexError)來印出一個字串中的最後一個字元。更大、更有系統的錯誤也可以被檢查出來。例如當使用者輸入大於100的數字、或是在網站檢索不可用的時候掛起此網站的話,程式就會一直崩潰。

這些所有的錯誤都可以透過對程式碼的仔細測試來檢查。 Unit testing,特別指在一個分隔的程式碼單元中的測試。一個單元可以是整個模組,一個單獨的類別或函數,或這兩者間的任何程式碼。然而,重要的是,測試程式碼要與我們沒有測試到的其他程式碼相互隔離(因為其它程式碼本身有錯誤的話會因此混淆測試結果)。考慮以下範例:

def is_prime(number):
    """Return True if *number* is prime."""
    for element in range(number):
        if number % element == 0:
            return False
    return True
def print_next_prime(number):
    """Print the closest prime number larger than *number*."""
    index = number
    while True:
        index += 1
        if is_prime(index):
            print(index)

   

你有兩個函數,is_prime 和 print_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 中最基本的內容,例如下面這個測試範例:

import unittest
from primes import is_prime
class PrimesTestCase(unittest.TestCase):
    """Tests for `primes.py`."""
    def test_is_five_prime(self):
        """Is five successfully determined to be prime?"""
        self.assertTrue(is_prime(5))
if __name__ == '__main__':
    unittest.main()

   

這個檔案透過一個test case:?test_is_five_prime. 建立了一個單元測試。透過Python內嵌的一個測試框架 unittest。當unittest.main()被呼叫時,任何一個以test開頭命名的成員函數將被運行,他們是unittest.TestCase的一個派生類,並且是斷言檢查的。如果我們透過輸入python test_primes.py來運行測試,我們能夠看到unittest框架在控制台上的輸出:

$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
    self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
    if number % element == 0:
ZeroDivisionError: integer division or modulo by zero
----------------------------------------------------------------------
Ran 1 test in 0.000s

   

單獨的「E」表示的是我們單元測試的結果(如果它成功了,會列印出一個“.”)。我們可以看到我們的測試失敗了,以及導致失敗的那行程式碼,還有任何引發的異常訊息。

為什麼要測試?

在我們繼續那個例子之前,要問個很重要的問題:「為什麼測驗對我來說有價值」?這是個公平的問題,也是那些對於程式碼測試不熟悉的人常問的問題。畢竟,測試需要一定的時間,而我們完全可以用這些時間去編程式碼,為什麼要測試而不是去做那些最有生產效率的事?

有很多答案可以有效的回答這個問題,我列出了以下幾點:

測試可以保證你的程式碼在一系列給定條件下正常工作

測試確保了一系列條件下的正確性。語法錯誤基本上一定透過測試被查出來,一個代碼單元的基本的邏輯也可以透過測試被檢測出來,以確保一定條件下的正確性。再一次,它不是要證明程式碼是在任何條件下都正確的。我們只是簡單的瞄準了一套比較完整的可能的條件(例如,你可以寫一個測試來監測當你調用my_addition_function(3, 'refrigerator), 的時候,但你不必為每個參數檢測所有可能的字符串)

測試允許人們確保對程式碼的變更不會破壞現有的功能

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

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

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

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

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

单元测试的剖析

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

def is_prime(number):
    """Return True if *number* is prime."""
    for element in range(number):
        if number % element == 0:
            return False
    return True
def print_next_prime(number):
    """Print the closest prime number larger than *number*."""
    index = number
    while True:
        index += 1
        if is_prime(index):
            print(index)

   

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

import unittest
from primes import is_prime
class PrimesTestCase(unittest.TestCase):
    """Tests for `primes.py`."""
    def test_is_five_prime(self):
        """Is five successfully determined to be prime?"""
        self.assertTrue(is_prime(5))
if __name__ == '__main__':
    unittest.main()

   

做出断言

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

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

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

$ python test_primes.py
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK

   

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

异常

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

$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
    self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
    if number % element == 0:
ZeroDivisionError: integer division or modulo by zero
----------------------------------------------------------------------
Ran 1 test in 0.000s

   

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

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

解决问题

一次失败的测试使我们修改了代码。一旦我们改好了这个错误(将s_prime中的一行改为for element in range(2, number):),我们就得到了如下输出:

$ python test_primes.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

   

现在错误已经改了,这是不是意味着我们应该删掉test_is_five_prime这个测试方法(因为很明显,它将不会一直能通过测试)?不应该删。由于通过测试是最终目标的话单元测试应该尽量少的被删除。我们已经测试过is_prime的语法是有效的,并且,至少在一种情况下,它返回正确的结果。我们的目标是要建立一套能全部通过的(单元测试的逻辑分组)测试,虽然有些一开始可能会失败。

test_is_five_prime用于处理一个“非特殊”的素数。让我们确保它也能正确处理非素数。将以下方法添加到PrimesTestCase类:

def test_is_four_non_prime(self):
    """Is four correctly determined not to be prime?"""
    self.assertFalse(is_prime(4), msg='Four is not prime!')

   

请注意,这时我们给assert调用添加了可选的msg参数。如果该测试失败了,我们的信息将被打印到控制台,并给运行测试的人提供额外的信息。

边界情况

我们已经成功的测试了两种普通情况。现在让我们考虑边界情况下、或者那些不寻常或意外的输入的用例。当测试一个其范围是正整数的函数时,边界情况下的实例包括0、1、负数和一个很大的数字。现在让我们来测试其中的一些。

添加一个对0的测试很简单。我们预计?is_prime(0)返回的是false,因为,根据定义,素数必须大于1。

def test_is_zero_not_prime(self):
    """Is zero correctly determined not to be prime?"""
    self.assertFalse(is_prime(0))

   

可惜呀,输出是:

python test_primes.py
..F
======================================================================
FAIL: test_is_zero_not_prime (__main__.PrimesTestCase)
Is zero correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 17, in test_is_zero_not_prime
    self.assertFalse(is_prime(0))
AssertionError: True is not false
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)

   

0被错误的判定为素数。我们忘记了,我们决定在数字范围中跳过0和1。让我们增加一个对他们的特殊检查。

def is_prime(number):
    """Return True if *number* is prime."""
    if number in (0, 1):
        return False
    for element in range(2, number):
        if number % element == 0:
            return False
    return True

   

现在测试通过了。我们的函数应该怎样处理一个负数?在写这个测试用例之前就知道输出结果是很重要的。在这种情况下,任何负数都应该返回false。

def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    for index in range(-1, -10, -1):
        self.assertFalse(is_prime(index))

   

这里我们觉得检查从-1到-9的所有数字。在一个循环中调用test方法是非常合法的,在一个测试中多次调用断言方法也可以。我们可以在下面用(更详细)的方式改写代码。

def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    self.assertFalse(is_prime(-1))
    self.assertFalse(is_prime(-2))
    self.assertFalse(is_prime(-3))
    self.assertFalse(is_prime(-4))
    self.assertFalse(is_prime(-5))
    self.assertFalse(is_prime(-6))
    self.assertFalse(is_prime(-7))
    self.assertFalse(is_prime(-8))
    self.assertFalse(is_prime(-9))

   

这两个是完全等价的。除了当我们运行循环版本时,我们得到了一个我们不太想要的信息:

python test_primes.py
...F
======================================================================
FAIL: test_negative_number (__main__.PrimesTestCase)
Is a negative number correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 22, in test_negative_number
    self.assertFalse(is_prime(index))
AssertionError: True is not false
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=1)

   

嗯···我们知道测试失败了,但是是在哪个负数上失败的?非常没用的是,Python的单元测试框架并没有打印出预期值和实际值。我们可以移步到两种方式上,并用其中之一来解决问题:通过msg参数,或通过使用一个第三方的单元测试框架。

使用msg参数来assertFalse仅仅能够使我们认识到我们可以用字符串的格式设置来解决问题。

def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    for index in range(-1, -10, -1):
        self.assertFalse(is_prime(index), msg='{} should not be determined to be prime'.format(index))

   

从而给出了如下输出信息:

python test_primes
...F
======================================================================
FAIL: test_negative_number (test_primes.PrimesTestCase)
Is a negative number correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "./test_primes.py", line 22, in test_negative_number
    self.assertFalse(is_prime(index), msg='{} should not be determined to be prime'.format(index))
AssertionError: True is not false : -1 should not be determined to be prime
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=1)

   

妥善地修复代码

我们看到,失败的负数是第一个数字:-1。为了解决这个问题,我们可以为负数增再增加一个特殊检查,但是编写单元测试的目的不是盲目的添加代码来检测边界情况。当一个测试失败时,我们应该退后一步并且确定解决问题的最佳方式。在这种情况下,我们就不该增加一个额外的if:

def is_prime(number):
    """Return True if *number* is prime."""
    if number < 0:
        return False
    if number in (0, 1):
        return False
    for element in range(2, number):
        if number % element == 0:
            return False
    return True

   

应当首先使用如下代码:

def is_prime(number):
    """Return True if *number* is prime."""
    if number <= 1:
        return False
    for element in range(2, number):
        if number % element == 0:
            return False
    return True

   

在后一个代码中,我们发现如果参数小于等于1时,两个if语句可以合并到一个返回值为false的语句中。这样做不仅更加简洁,并且很好的贴合了素数的定义(一个比1大并且只能被1和它本身整除的数)。

第三方测试框架

我们本来也可以通过使用第三方测试框架解决这个由于信息太少导致测试失败的问题。最常用的两个是py.test和nose。通过运行语句py.test -l(-l为显示局部变量的值)可以得到如下结果。

#! bash
py.test -l test_primes.py
============================= test session starts ==============================
platform linux2 -- Python 2.7.6 -- pytest-2.4.2
collected 4 items
test_primes.py ...F
=================================== FAILURES ===================================
_____________________ PrimesTestCase.test_negative_number ______________________
self =     def test_negative_number(self):
        """Is a negative number correctly determined not to be prime?"""
        for index in range(-1, -10, -1):
>           self.assertFalse(is_prime(index))
E           AssertionError: True is not false
index      = -1
self       = test_primes.py:22: AssertionError


正如你所看到的,一些更有用的信息。这些框架提供了比单纯的更详细的输出更多的功能,但问题是仅仅知道它们能存在和扩展内置unittest测试包的功能。

结束语

在这篇文章中,你学到了什么是单元测试,为什么它们如此重要,还有怎样编写测试。这就是说,要注意我们只是剖开了测试方法学中的表层,更多高级的话题,比如测试案例的组织、持续整合以及测试案例的管理等都是可供那些想要进一步学习Python中的测试的读者研究的很好的话题。

在不改变其功能的前提下重组/清理代码

编代码时不暴露其内部数据或函数并且不使用其他代码的内部数据或函数


文章转自:http://blog.jobbole.com/55180/ 作者:卷卷怪

英文出处:http://jeffknupp.com/blog/2013/12/09/improve-your-python-understanding-unit-testing/


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn