《Python Testing Cookbook》讀書筆記之一:單元測試
Python Testing Cookbook
讀書筆記
python
testing
Chapter 1: Using Unittest To Develop Basic Tests
配置虛擬環境
在開始寫程式碼測試前,先建立一個獨立的測試開發環境,這樣可以避免各種包和現有開發環境互相影響,適合進行測試。
一般可以通過virtualenv
來建立虛擬環境,這裡是官方文件和一篇寫得比較好的中文版指南。如果你和我一樣使用Anaconda
的Python發行版的話,可以使用conda
create
命令來進行操作,指南戳這裡。
Anaconda 是一個用來進行大規模資料處理,預測分析和科學計算的Python發行包,裡面內建了iPython,NumPy,SciPy等近200種常用包,如果你用python用來做這些事情比較多的話,建議可以直接下載這個。官方地址:
https://store.continuum.io/cshop/anaconda/
Asserting the basics
使用例子:
class RomanNumeralConverter(object):
def __init__(self, roman_numeral):
self.roman_numeral = roman_numeral
self.digit_map = {"M":1000, "D":500, "C":100, "L":50, "X":10, "V":5, "I":1}
def convert_to_decimal(self):
val = 0
for char in self.roman_numeral:
val += self.digit_map[char]
return val
import unittest
class RomanNumeralConverterTest(unittest.TestCase):
def test_parsing_millenia(self):
value = RomanNumeralConverter("M")
self.assertEquals(1000, value.convert_to_decimal())
def test_parsing_century(self):
value = RomanNumeralConverter("C")
self.assertEquals(100, value.convert_to_decimal())
def test_parsing_half_century(self):
value = RomanNumeralConverter("L")
self.assertEquals(50, value.convert_to_decimal())
def test_parsing_decade(self):
value = RomanNumeralConverter("X")
self.assertEquals(10, value.convert_to_decimal())
def test_parsing_half_decade(self):
value = RomanNumeralConverter("V")
self.assertEquals(5, value.convert_to_decimal())
def test_parsing_one(self):
value = RomanNumeralConverter("I")
self.assertEquals(1, value.convert_to_decimal())
def test_empty_roman_numeral(self):
value = RomanNumeralConverter("")
self.assertTrue(value.convert_to_decimal() == 0)
self.assertFalse(value.convert_to_decimal() > 0)
def test_no_roman_numeral(self):
value = RomanNumeralConverter(None)
self.assertRaises(TypeError, value.convert_to_decimal)
if __name__ == "__main__":
unittest.main()
-
使用方法
- 建立測試類,命名方式為要測試的類名+Test,並繼承unittest類,如
ClassXxxTest(unittest.TestCase)
- 在測試類中建立測試方法,如
test_xxx_xxx
方法 - 在類外
import unittest
,並在主程式入口執行unittest.main()
- 建立測試類,命名方式為要測試的類名+Test,並繼承unittest類,如
-
基本的
assert
語句:
assertEquals(first, second[, msg])
assertTrue(expression[, msg])
assertFalse(expression[, msg])
assertRaises(exception, callable, ...)
-
儘量使用
assertEquals
語句而非assertTrue
和assertFalse
,因為其他幾個只會報錯,而assertEquals
可以顯示兩個值分別是多少,提供了更多的資訊。 -
Unittest可以使用
self.fail([msg])
來產生失敗測試,但儘量使用assert
語句改寫,因為這個語句在測試正確的情況下用不到。
使用setUp
和tearDown
函式在測試前後進行有關處理
如需要每個測試都需要新建例項進行初始化操作的話,可以定義在setUp
中;需要在測試中開啟檔案的話,在tearDown
中使用close
方法。
將測試類打包成test suite
進行測試
if __name__ == "__main__":
suite = unittest.TestLoader().loadTestsFromTestCase( \
RomanNumeralConverterTest)
unittest.TextTestRunner(verbosity=2).run(suite)
在測試方法中插入註釋資訊,在每一次該方法執行和失敗時顯示
def test_parsing_century(self):
"This test method is coded to fail for demo."
value = RomanNumeralConverter("C")
self.assertEquals(10, value.convert_to_decimal())
執行一部分測試用例
當測試例子變得很多的時候,每一次都全部執行需要花費很長的時間,此時我們可以用這個方法執行一部分測試用例。
if __name__ == "__main__":
import sys
suite = unittest.TestSuite()
if len(sys.argv) == 1:
suite = unittest.TestLoader().loadTestsFromTestCase(\
RomanNumeralConverterTest)
else:
for test_name in sys.argv[1:]:
suite.addTest(\
RomanNumeralConverterTest(test_name))
unittest.TextTestRunner(verbosity=2).run(suite)
用獨立的test檔案測試幾個test suite
這是將幾個test suite都放在主程式中
if __name__ == "__main__":
import unittest
from recipe5 import *
suite1 = unittest.TestLoader().loadTestsFromTestCase( \
RomanNumeralConverterTest)
suite2 = unittest.TestLoader().loadTestsFromTestCase( \
RomanNumeralComboTest)
suite = unittest.TestSuite([suite1, suite2])
unittest.TextTestRunner(verbosity=2).run(suite)
還可以將幾個不同的test suite定義在測試模組中
def combos():
return unittest.TestSuite(map(RomanNumeralConverterTest,\
["test_combo1", "test_combo2", "test_combo3"]))
def all():
return unittest.TestLoader().loadTestsFromTestCase(\
RomanNumeralConverterTest)
用以下方法呼叫所有的suite,如需呼叫某一個或某幾個,參照修改即可。不同的suite可以用來實現不同功能的測試。
if __name__ == "__main__":
for suite_func in [combos, all]:
print "Running test suite '%s'" % suite_func.func_name
suite = suite_func()
unittest.TextTestRunner(verbosity=2).run(suite)
將老的assert
測試程式碼改成單元測試程式碼
這是老的測試類
class RomanNumeralTester(object):
def __init__(self):
self.cvt = RomanNumeralConverter()
def simple_test(self):
print "+++ Converting M to 1000"
assert self.cvt.convert_to_decimal("M") == 1000
通過unittest.FunctionTestCase
方法將其轉換成unittest方法,然後新增到suite裡。傳統的assert方法在一個assert失敗後就會報錯退出,改成這種形式後,會將所有的測試用例都測試後才退出,並展示錯誤資訊。
import unittest
if __name__ == "__main__":
tester = RomanNumeralTester()
suite = unittest.TestSuite()
for test in [tester.simple_test, tester.combo_test1, \
tester.combo_test2, tester.other_test]:
testcase = unittest.FunctionTestCase(test)
suite.addTest(testcase)
unittest.TextTestRunner(verbosity=2).run(suite)
將有多個assertion
的複雜測試方法拆散成每次測試一個簡單功能的小測試方法
def test_convert_to_decimal(self):
self.assertEquals(0, self.cvt.convert_to_decimal(""))
self.assertEquals(1, self.cvt.convert_to_decimal("I"))
self.assertEquals(2010, self.cvt.convert_to_decimal("MMX"))
self.assertEquals(4000, self.cvt.convert_to_decimal("MMMM"))
應該寫成
def test_to_decimal1(self):
self.assertEquals(0, self.cvt.convert_to_decimal(""))
def test_to_decimal2(self):
self.assertEquals(1, self.cvt.convert_to_decimal("I"))
def test_to_decimal3(self):
self.assertEquals(2010, self.cvt.convert_to_decimal("MMX"))
def test_to_decimal4(self):
self.assertEquals(4000, self.cvt.convert_to_decimal("MMMM"))
這樣的好處是前者發生錯誤時只會報一個錯,且第一個assert語句出錯時不會執行後面的測試,而第二種方法會檢測所有用例,並給出詳細的錯誤統計。
如果我們有很多組要測試的值,這裡面會出現大量的重複程式碼,有沒有簡單點兒的方法呢?我們可以手動改變python的名稱空間實現批量加入函式.
先來看一段程式碼:
def make_add(n):
def func(x):
return x+n
return func
if __name__ == '__main__':
for i in xrange(1,10):
locals()['add_%d'%i] = make_add(i)
print add_1(7)
print add_9(19)
在python中函式可以作為引數傳遞,所以make_add
方法可以生成一個加n的函式返回。而在主程式的迴圈裡,我們將add_n
方法通過make_add
函式來生成,再通過加入locals()
新增到本地名稱空間,這相當於在本地建立了從add_1
到add_9
的9個函式。
這個搞明白後,就可以動手改寫前面的程式碼了。
v_s = [
(1000, "M"),
(100, "C"),
(50, "L"),
(10, "X"),
(5, "V"),
(1, "I"),
]
def make_test(v, s):
def func(self):
value = RomanNumeralConverter(s)
self.assertEquals(v, value.convert_to_decimal())
return func
for for i, (j, k) in enumerate(v_s, 1):
locals()['test_to_decimal%d' % i] = make_test(v, s)
這段程式碼可以實現前面第二種寫法的功能,當你想要新增新的測試用例時,只需在列表v_s
中新增即可。
通過迭代實現批量測試
當測試用例很多的時候,還可以使用下面的方法實現批量新增。我們自己寫了一個生成assert語句的函式。但這種情況類似於上面講過的第一種方法,即將很多assert
語句寫在了同一個測試函式中,如果有一個發生錯誤,它後面的例子都不會被測試。
def test_bad_inputs(self):
r = self.cvt.convert_to_roman
d = self.cvt.convert_to_decimal
edges = [("equals", r, "", None),\
("equals", r, "I", 1.2),\
("raises", d, TypeError, None),\
("raises", d, TypeError, 1.2)\
]
[self.checkout_edge(edge) for edge in edges]
def checkout_edge(self, edge):
if edge[0] == "equals":
f, output, input = edge[1], edge[2], edge[3]
print("Converting %s to %s..." % (input, output))
self.assertEquals(output, f(input))
elif edge[0] == "raises":
f, exception, args = edge[1], edge[2], edge[3:]
print("Converting %s, expecting %s" % \
(args, exception))
self.assertRaises(exception, f, *args)