Python 的檢查及測試工具箱

Patzie!
PyLadies Taiwan
Published in
16 min readAug 27, 2018

TL; DR (Too Long; Didn’t Read) —
unittest 和 doctest 是最基本的內建工具,小型程式、專案都可直接使用;unittest 的 setUp() 和 tearDown() 方法,適合用在比較複雜的執行環境的情況下。doctest 一方面是測試程式碼,一方面也用來確認 docStrings 的內容沒有過期。而 py.test 的強項則是有很多 plug-in 可以擴充,輔助各類專案。

不論我們的程式碼是要自己用,或是為別人服務,為程式碼寫測試都是非常重要的。開發者常常有這樣的經驗:就算只對程式做一點點改變、而且再三拍胸脯保證這些改變不會影響到其他東西,事實上(很不幸地)就是會影響到 (ಠ_ರೃ。

什麼是測試程式?

如果妳曾經在任一個線上程式學習平台練習題目(例如 CheckIOHackerRank),寫完程式碼按下 Submit 那一刻,平台系統就會開始跑他們的測試程式,接著告訴我們幾個 Test case 通過、幾個失敗,讓我們針對失敗的 Test case 進行除錯。

現在我們已經不是寫題目的年紀而已了!我們寫了自己的程式碼,Test case 和測試程式當然也得自己來。要寫出好的程式,一邊寫主要程式、一邊寫測試程式可能是我們想要養成的習慣,它能幫助我們了解自己寫出來的程式是不是以我們想像的方式運作。

測試 Python 程式最簡單的方式,就是每次丟入不同的 Test case,接著在程式中間加入 print() ,把想知道的變數內容印出來檢查。但之後妳得記得把 print()全部移除,這時還可能會發生剪下貼上問題,把程式碼弄得一團糟!所以我們會另外寫測試程式,接下來會提到一些簡單的 Python 測試工具,以及該如何使用他們。

pylint:開始測試之前,先檢查一下格式吧

如果妳只想很快了解一下 Python 的測試套件,可以跳過這一個小節。

測試程式可以用來確認主要程式是否有問題、邏輯是否正確,但並不會檢查程式格式是否正確、有沒有遵守 Python 的程式寫作規範(Coding standard)、有沒有程式碼重複狀況。

以上檢查可以由 pylint 代勞,避免我們踩到 Python 美感的地雷。pylint 是熱門的 Python 原始碼靜態分析器,它相當於自動強制執行 PEP 8( Python 的程式碼風格指南),同時也會偵測其他類型的常見錯誤,像是有沒有忘記匯入模組。

首先請在終端機安裝 pylint:

$pip install pylint

我們儲存一小段有錯的程式碼,檔名為 example1.py:

a = 1
b = 2
print(a)
print(b)
print(c)

現在在終端機請 pylint 出動之後,它輸出這樣的回應:

$pylint example1.pyNo config file found, using default configuration
************* Module example1
C: 1, 0: Missing module docstring (missing-docstring)
C: 1, 0: Constant name "a" doesn't conform to UPPER_CASE naming style (invalid-name)
C: 2, 0: Constant name "b" doesn't conform to UPPER_CASE naming style (invalid-name)
E: 5, 6: Undefined variable 'c' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -6.00/10

真是有趣!pylint 會為我們的程式碼打分數,不過現在我們的分數顯然很糟糕 Your code has been rated at -6.00/10 ,只有 -6 分!來看看我們被挑出什麼問題。

E: 5,6: Undefined variable 'c' (undefined-variable) 這行告訴我們有 Error 產生,因為我們根本沒有指派一個值給 c 。我們在原來的程式碼中加上 c = 3

C: 1, 0:C: 2, 0: 開頭的訊息告訴我們,pylint 希望在程式中加入一個 docstring(位於 module 或 function 的最上方,用來說明該段程式碼的短文);另外,變數名稱 a, b, c 不符合變數命名規範(補充於本節節末)。我們把 example1.py 修改一下:

"Place module docstring here"FIRST = 1
SECOND = 2
THIRD = 3
print(FIRST)
print(SECOND)
print(THIRD)

現在再次請 pylint 出動,它輸出這樣的回應:

$pylint example1.pyNo config file found, using default configuration--------------------------------------------------------------------
Your code has been rated at 10.00/10(previous run: -6.00/10, +16.00)

這次 pylint 不再挑出錯誤,終於拿到滿分分數了!萬歲!

根據 Python 的程式碼風格指南 PEP 8,部分命名規範如下:

  • 套件或模組:全部使用小寫並保持簡短,例如 numpy, pandas, os
  • 類別名稱:字串首字母大寫,例如 Testcase
  • 全域變數名稱和函數名稱:全部使用小寫以及下劃線,例如: num_of_questions, create_test()
  • 常數變數名稱:全部使用大寫以及下劃線,例如: SECOND_PER_MINUTE

檢查完程式的格式之後,可以開始測試了。Python 的標準程式庫有兩組測試套件,一是現在要介紹的 unittest,另一個是待會要提到的 doctest。

測試工具:unittest

測試某個函式,就是設定對函式送進某些輸入後,妳希望得到什麼輸出。在 unittest 中,這樣的設定會寫在另一個命名以 test_ 開頭的 Python 檔案裡(當然,不遵守這個命名方式程式還是可以運作,但是待會我們會很慶幸有遵守這個命名方式)。

我們來測試一個把單字字首改成大寫的模組,把這存成 cap.py

def just_do_it(text):
return text.capitalize()

現在另外開一個測試檔,存成 test_cap.py

import unittest
import cap
class TestCap(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_one_word(self):
text = 'duck'
result = cap.just_do_it(text)
self.assertEqual(result, 'Duck')
def test_multiple_words(self):
text = 'a veritable flock of ducks'
result = cap.just_do_it(text)
self.assertEqual(result, 'A Veritable Flock Of Ducks')
if __name__ == '__main__':
unittest.main()
  • 測試檔裡建立一個 TestCap 類別,且這個類別要繼承 unittest.TestCase,再把 Test case 包在 TestCap 類別的方法中。
  • test_one_word()test_multiple_words() 是我們的檢查內容。函式回傳的結果是否合乎預期,要用 unittest 提供的判斷提示方法(Assertion)檢查。 — 請注意,python 也有自己的判斷提示方法(參考 Assertions in Python),但在這裡我們遵守 unittest 提供的方法 — 此例使用 assertEqual(a, b)檢查 a == b 是否正確,若不符合則會發生錯誤。其他的檢查方法如 assertNotEqual, assertTrue... 請參考 unittest 文件
  • setUp()tearDown() 方法分別會在測試的一開始及最後結束時執行,他們的目的通常是為了配置及釋出外部資源,例如某些測試資料的資料庫連結。(這個例子不需要用到所以沒有定義)

現在來執行看看:

$python test_cap.pyF.
====================================================================
FAIL: test_multiple_words (__main__.TestCap)
--------------------------------------------------------------------
Traceback (most recent call last):
File "test_cap.py", line 19, in test_multiple_words
self.assertEqual(result, 'A Veritable Flock Of Ducks')
AssertionError: 'A veritable flock of ducks' != 'A Veritable Flock Of Ducks'
- A veritable flock of ducks
? ^ ^ ^ ^
+ A Veritable Flock Of Ducks
? ^ ^ ^ ^
--------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1)

Assertion 判斷式提醒我們,第二個測試 test_multiple_words 失敗了,函式執行結果和我們設定的結果不同。那我們把原來的 cap.py 修改一下:

def just_do_it(text):
from string import capwords
return capwords(text)

再執行一次:

$python test_cap.py..
--------------------------------------------------------------------
Ran 2 tests in 0.000s
OK

測試通過了!

測試工具:doctest

藉由 doctest 這個套件,我們可以直接在 docstring 裡面邊寫測試,而不用另外寫一個測試檔。至於寫在 docstring 有什麼好處,什麼時候要把測試寫在 docstring,會在這篇文章的最後討論。

我們先來把剛才的 cap.py 改寫成使用 doctest 套件:

def just_do_it(text):
"""
>>> just_do_it('duck')
'Duck'
>>> just_do_it('a veritable flock of ducks')
'A Veritable Flock Of Ducks'
"""
from string import capwords
return capwords(text)
if __name__ == '__main__':
import doctest
doctest.testmod()

docstring 是 位於 module 或 function 的最上方,用來說明該段程式碼的短文。docstring 的測試格式跟互動式解譯器長得很像(先有 >>> + 呼叫式,之後是結果)。

最後執行程式,若有錯誤同樣會出現錯誤訊息;若沒有發生問題程式就會直接執行完畢,不會留下訊息。我們也可以透過詳細資訊選項(-v)要求展示沒有錯誤的程式的執行過程:

$python cap.py -vTrying:
just_do_it('duck')
Expecting:
'Duck'
ok
Trying:
just_do_it('a veritable flock of ducks')
Expecting:
'A Veritable Flock Of Ducks'
ok
1 items had no tests:
__main__
1 items passed all tests:
2 tests in __main__.just_do_it
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

測試工具:py.test

除了 unittest 和 doctest 這兩個標準程式庫內建的模組,py.test 也是很受歡迎的測試工具,不過必須另外下載。在終端機輸入:

$pip install -U pytest

事實上,剛才我們用 unittest 寫的測試檔完全可以給 py.test 使用,只要剛才有遵守命名規範,以 test_*.py 命名檔案。py.test 預設會搜尋該路徑中所有的 test_*.py檔或 *_test.py檔並執行測試。(註:也可以設定 pytest.ini,指定用其他非 test_ 開頭的檔案名和 func 名當測試檔,參考官方文件中 Changing naming conventions 部分)

動手試試看,在剛才路徑輸入:

$py.test====================== test session starts ======================
platform darwin -- Python 3.6.4, pytest-3.6.1, py-1.5.3, pluggy-0.6.0
rootdir: /Users/username/Desktop, inifile:
collected 2 items
test_cap.py .. [100%]
=================== 2 passed in 1.55 seconds =====================

輸出的格式不同,但內容和 unittest 沒什麼兩樣。

那 py.test 究竟和 unittest 有什麼差別呢?他們不同的地方如下:

  • py.test 使用 Python 的原生判斷關鍵字 assert,而非 unittest 的 assertion 方法(關鍵字 assert,可以參考 Assertions in Python
  • py.test 不用像 unittest 那樣把所有的 Test case 包在一個 subclass 裡(不過包在一個 subclass 裡也是可以的)
  • 下了 py.test 命令之後,py.test 會遞迴搜尋該路徑及路徑下的子目錄中所有以 test_ 開頭的測試檔,或者 pytest.ini 中指定的檔名或 func 名

現在我們把剛才的test_cap.py 改寫成比較符合 py.test 使用的版本:

import capdef test_one_word():
text = 'duck'
result = cap.just_do_it(text)
assert result == 'Duck'
def test_multiple_words():
text = 'a veritable flock of ducks'
result = cap.just_do_it(text)
assert result == 'A Veritable Flock Of Ducks'
if __name__ == '__main__':
main()

執行看看,成功了!這次每個 Test case 都是一個函式,不再需要全部包成一個 subclass。

如果 Test case 不符合預期結果,py.test 也會詳細指出錯誤(畢竟是測試程式的重要功能)。我們把 assert result == 'A veritable flock of ducks' ,馬上得到錯誤訊息:

➜  Desktop py.test
====================== test session starts ======================
platform darwin -- Python 3.6.4, pytest-3.6.1, py-1.5.3, pluggy-0.6.0
rootdir: /Users/username/Desktop, inifile:
collected 2 items
test_cap.py .F [100%]============================ FAILURES ============================
________________________ test_multiple_words _____________________
def test_multiple_words():
text = 'a veritable flock of ducks'
result = cap.just_do_it(text)
> assert result == 'A veritable flock of ducks'
E AssertionError: assert 'A Veritable Flock Of Ducks' == 'A veritable flock of ducks'
E - A Veritable Flock Of Ducks
E ? ^ ^ ^ ^
E + A veritable flock of ducks
E ? ^ ^ ^ ^
test_cap.py:10: AssertionError================ 1 failed, 1 passed in 1.55 seconds ================

比較 unittest / doctest / py.test

終於介紹完三個測試工具了!那… 什麼時候要用到哪一個工具呢?

  • unittest 和 doctest 是最基本的內建工具,小型程式、專案都可直接使用
  • unittest 的 setUp() 和 tearDown() 方法其實是很有用的特性,在測試開始前以及結束後執行,用來做測試準備和結束的工作,很適合用在需要複雜的執行環境的情況下。
  • doctest 一方面是測試程式碼,一方面也用來確認 docStrings 的內容沒有過期。有時程式碼經過積年累月的修改,docString 敘述卻沒同步更新,doctest 可以避免這種狀況帶來的誤會。
  • py.test 的強項則是有很多 plug-in 可以擴充,輔助各類專案。常被用到的幾個 plug-in 有:
  1. Distributed/parallelized:pytest-xdist,在 remote machine 上使用,或是用在本地機器的 parallel process 來加快速度
  2. Django:pytest-django,整合 django 專案的 tests
  3. Twisted:pytest-twisted
  4. Log capture:pytest-capturelog

(更多 plug-in 請參考 py.test 文件

參考資料及書籍

--

--