前言
為什么需要單元測試?
如果沒有單元測試,我們會遇到這種情況:已有的健康運行的代碼在經過改動之后,我們無法得知改動之后是否引入了Bug。如果有單元測試的話,只要單元測試全部通過,我們就可以保證沒有Bug被引入。因此,單元測試是保證軟件工程質量的一個很重要的方面。
Python中的單元測試
Python最強大的地方在於,開發效率高,並且有豐富的Package,避免重復造輪子。那么Python中的Unittest模塊有很豐富的功能提供給我們調用:完整的測試框架,豐富的拓展,比如我們可以設置測試之前的一些初始化工作,比如鏈接數據庫等,規划測試集中有哪些測試用例需要跳過,以及跳過的原因。
Unittest中幾個類(Class)的基本概念
TestCase 是我們要寫的具體的測試用例
TestSuite 多個測試用例集合在一起,中文翻譯過來叫測試套件,其實就是測試集。
TestLoader是用來加載TestCase到TestSuite中的(更通俗一點,就是用來把符合我們定義的條件的測試用例組合起來,成為一個測試集),一般會以參數的形式傳進去一些條件,比如收集某個目錄下所有的test case組成新的測試集。
TestRunner是來執行測試用例的,測試的結果會保存到TestResult實例中,包括運行了多少測試用例,成功了多少,失敗了多少等信息
一個簡單的測試例子
>>> class MyTest(unittest.TestCase): #Run before whole testcase set execution, decorator classmethod is essential @classmethod def setUpClass(self): print("UnitTest Begin...") #Run after whole testcase set execution, decorator classmethod is essential @classmethod def tearDownClass(self): print("UnitTest End...") #Run before each test case execution def setUp(self): print("Begin...") #Run after each test case execution def tearDown(self): print("End...") def test_1(self): self.assertEqual(1,1) def test_2(self): self.assertEqual(1,2) >>> if __name__ == '__main__':unittest.main() UnitTest Begin... Begin... End... .Begin... End... FUnitTest End... ====================================================================== FAIL: test_2 (__main__.MyTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "<pyshell#41>", line 15, in test_2 AssertionError: 1 != 2 ---------------------------------------------------------------------- Ran 2 tests in 0.097s FAILED (failures=1)
在這個例子中,有幾個函數要注意:
setUp()和tearDown():每個test case執行之前和執行之后要運行的操作:我們可以在這里定義測試的准備工作,比如鏈接數據庫,web登錄等等。
用裝飾器classmethod裝飾的setUpClass()和tearDownClass(): 跑類中定義的所有test cases之前和之后,需要執行的操作。
test_1()和test_2(),具體的測試用例,一定要以test為開頭,因為unittest框架中,定義為,如果TestCase類中以test為開頭的函數,將會作為具體的testcase收錄進要執行的測試集里。
self.assertEqual(),是TestCase類中的斷言函數,用來做判斷的,用以判斷該條測試用例是否通過。通過名字我們可以看出,這個函數的意思是判斷兩個值是否相等,如果相等,則用例通過,如果不等,則用例不通過。類似的,斷言函數還有很多:有一個msg參數,如果指定msg參數的值,則將該信息作為失敗的錯誤信息返回
三種常見測試寫法
第一種: 搜索該模塊下所有以test開頭的測試用例方法,並自動執行它們
#執行測試用例方案一如下: #unittest.main()方法會搜索該模塊下所有以test開頭的測試用例方法,並自動執行它們。 import unittest #定義測試類,父類為unittest.TestCase。 #可繼承unittest.TestCase的方法,如setUp和tearDown方法,不過此方法可以在子類重寫,覆蓋父類方法。 #可繼承unittest.TestCase的各種斷言方法。 class Test(unittest.TestCase): def setUp(self): self.number=raw_input('Enter a number:') self.number=int(self.number) #定義測試用例,以“test_”開頭命名的方法 #可使用unittest.TestCase類下面的各種斷言方法用於對測試結果的判斷 def test_case1(self): print self.number self.assertEqual(self.number,10,msg='Your input is not 10') def test_case2(self): print self.number self.assertEqual(self.number,20,msg='Your input is not 20') @unittest.skip('暫時跳過用例3的測試') def test_case3(self): print self.number self.assertEqual(self.number,30,msg='Your input is not 30') def tearDown(self): print 'Test over' #如果直接運行該文件(__name__值為__main__),則執行以下語句,常用於測試腳本是否能夠正常運行 if __name__=='__main__': #執行順序是命名順序:先執行test_case1,再執行test_case2 unittest.main()
第二種: 構造測試集,實例化test suite(即TestRunner類), 運行test suite中所有的測試用例。
''' 執行測試用例方案二如下: 先構造測試集 實例化測試套件 ''' suite=unittest.TestSuite() #將測試用例加載到測試套件中。 #執行順序是安裝加載順序:先執行test_case2,再執行test_case1 suite.addTest(Test('test_case2')) suite.addTest(Test('test_case1')) #執行測試用例 #實例化TextTestRunner類 runner=unittest.TextTestRunner() #使用run()方法運行測試套件(即運行測試套件中的所有用例) runner.run(suite)
第三種:通過收集指定目錄下的目標測試用例,構造測試集再執行
#構造測試集(簡化了方案二中先要創建測試套件然后再依次加載測試用例) #執行順序同方案一:執行順序是命名順序:先執行test_case1,再執行test_case2 test_dir = './' discover = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py') #執行測試用例 #實例化TextTestRunner類 runner=unittest.TextTestRunner() #使用run()方法運行測試套件(即運行測試套件中的所有用例) runner.run(discover)
如何生成HTML和XML格式的測試報告
上面我們得到的測試結果是文本格式的,可讀性不好,並且也無法直接用來作后續的測試結果數據處理,比如測試結果的分類統計等等。
那么有兩種方式可供我們選擇:HTML和XML
HTML格式的報告,就是網頁格式,可讀性會比較好。XML格式的報告,則比較方便作后續的數據處理。
需要注意的是,我們需要安裝額外的package,即HtmlTestRunner和xmlrunner。
在配置好Pip的前提下,可以通過以下命令安裝:
pip install html-testrunner
pip instll xmlrunner
如果沒有配置好pip或者用pip安裝失敗,則需要用以下方式安裝(xmlrunner同理):
#!/usr/bin/python3 # -*- coding: utf-8 -*- import unittest import HtmlTestRunner class TestStringMethods(unittest.TestCase): def test_upper(self): self.assertEqual('foo'.upper(),'FOO') def test_isupper(self): self.assertFalse('Foo'.isupper()) def test_split(self): s = 'hello world' self.assertEqual(s.split(),['hello','world']) with self.assertRaises(TypeError): s.split(2) if __name__ == '__main__': suite = unittest.TestSuite() suite.addTest(TestStringMethods('test_upper')) suite.addTest(TestStringMethods('test_isupper')) suite.addTest(TestStringMethods('test_split')) runner = HtmlTestRunner.HTMLTestRunner(output='MyUnitTest') runner.run(suite)
最終我們會得到一個可讀性比較好的網頁報告。
XML報告:
有時我們需要得到格式化數據的測試報告,此時XML格式就要比HTML格式好的多。
因為XML格式的test result容易被讀取和數據處理。
示例代碼如下:
#!/usr/bin/python3 # -*- coding: utf-8 -*- import unittest import xmlrunner class TestStringMethods(unittest.TestCase): def test_upper(self): self.assertEqual('foo'.upper(),'FOO') def test_isupper(self): self.assertFalse('Foo'.isupper()) def test_split(self): s = 'hello world' self.assertEqual(s.split(),['hello','world']) with self.assertRaises(TypeError): s.split(2) if __name__ == '__main__': suite = unittest.TestSuite() suite.addTest(TestStringMethods('test_upper')) suite.addTest(TestStringMethods('test_isupper')) suite.addTest(TestStringMethods('test_split')) #fp = open('result.html','w') runner = xmlrunner.XMLTestRunner(output='MyUnitTest') #runner = HtmlTestRunner.HTMLTestRunner(stream=fp,output='MyUnitTest') runner.run(suite)
得到的結果是這樣的:
<?xml version="1.0"?> -<testsuite time="0.000" tests="3" name="TestStringMethods-20181115000346" failures="0" errors="0"> <testcase time="0.000" name="test_upper" classname="TestStringMethods"/> <testcase time="0.000" name="test_isupper" classname="TestStringMethods"/> <testcase time="0.000" name="test_split" classname="TestStringMethods"/> -<system-out> <![CDATA[]]> </system-out> -<system-err> <![CDATA[]]> </system-err> </testsuite>
幾個利用unittest做測試的實際例子
百度搜索測試用例
from selenium import webdriver import unittest, time class BaiduTest(unittest.TestCase): def setUp(self): self.driver = webdriver.Firefox() self.driver.implicitly_wait(30) #隱性等待時間為30秒 self.base_url = "https://www.baidu.com" def test_baidu(self): driver = self.driver driver.get(self.base_url + "/") driver.find_element_by_id("kw").clear() driver.find_element_by_id("kw").send_keys("unittest") driver.find_element_by_id("su").click() time.sleep(3) title=driver.title self.assertEqual(title, u"unittest_百度搜索") def tearDown(self): self.driver.quit() if __name__ == "__main__": unittest.main()
有道翻譯測試用例
from selenium import webdriver import unittest, time class YoudaoTest(unittest.TestCase): def setUp(self): self.driver = webdriver.Firefox() self.driver.implicitly_wait(30) #隱性等待時間為30秒 self.base_url = "http://www.youdao.com" def test_youdao(self): driver = self.driver driver.get(self.base_url + "/") driver.find_element_by_id("translateContent").clear() driver.find_element_by_id("translateContent").send_keys(u"你好") driver.find_element_by_id("translateContent").submit() time.sleep(3) page_source=driver.page_source self.assertIn( "hello",page_source) def tearDown(self): self.driver.quit() if __name__ == "__main__": unittest.main()
web測試用例:通過測試套件TestSuite來組裝多個測試用例。
import unittest from test_case import test_baidu from test_case import test_youdao #構造測試集 suite = unittest.TestSuite() suite.addTest(test_baidu.BaiduTest('test_baidu')) suite.addTest(test_youdao.YoudaoTest('test_youdao')) if __name__=='__main__': #執行測試 runner = unittest.TextTestRunner() runner.run(suite)
參考鏈接:
2. unittest
— Unit testing framework https://docs.python.org/3/library/unittest.html