一談及unittest,大家都知道,unittest是Python中自帶的單元測試架構,它裡面封裝好了一些校驗傳回的結果方法和一些用例執行前的初始化操作。unittest單元測試架構不僅可以适用于單元測試,還可以适用web自動化測試用例的開發與執行,該測試架構可組織執行測試用例,并且提供了豐富的斷言方法,判斷測試用例是否通過,最終生成測試結果。
在聊unittest時,需要先明白,最基礎的四個概念:TestCase,TestSuite,TestRunner,TestFixture,看如下靜态圖:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuIDNxcDMyYTN20SOzIDO0IzM3EjNykDM5EDMy0yNyIjM0ITMvwVOwkTMwIzLcdjMyIDNyEzLcd2bsJ2Lc12bj5ycn9Gbi52YugTMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
unittest運作流程
先編寫好TestCase,然後由TestLoader加載TestCase到TestSuite,其次由TextTestRunner來運作TestSuite,運作的結果儲存在TextTestResult中,我們通過指令行或者unittest.main()執行時,main會調用TextTestRunner中的run來執行,或者我們可以直接通過TextTestRunner來執行用例。
unittest子產品的各個屬性說明
unittest.TestCase:TestCase類,所有測試用例類繼承的基本類。
class BaiduTest(unittest.TestCase):
unittest.main():使用它可以友善的将一個單元測試子產品變為可直接運作的測試腳本,main()方法使用TestLoader類來搜尋所有包含在該子產品中以“test”命名開頭的測試方法,并自動執行他們。執行方法的預設順序是:根據ASCII碼的順序加載測試用例,數字與字母的順序為:0-9,A-Z,a-z。是以以A開頭的測試用例方法會優先執行,以a開頭會後執行。
unittest.TestSuite():unittest架構的TestSuite()類是用來建立測試套件的。
unittest.TextTextRunner():unittest架構的TextTextRunner()類,通過該類下面的run()方法來運作suite所組裝的測試用例,入參為suite測試套件。
unittest.defaultTestLoader(): defaultTestLoader()類,通過該類下面的discover()方法可自動更具測試目錄start_dir比對查找測試用例檔案(test*.py),并将查找到的測試用例組裝到測試套件,是以可以直接通過run()方法執行discover。用法如下:
discover=unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')
unittest.skip():裝飾器,當運作用例時,有些用例可能不想執行等,可用裝飾器暫時屏蔽該條測試用例。一種常見的用法就是比如說想調試某一個測試用例,想先屏蔽其他用例就可以用裝飾器屏蔽。
@unittest.skip(reason): skip(reason)裝飾器:無條件跳過裝飾的測試,并說明跳過測試的原因。
@unittest.skipIf(reason):skipIf(condition,reason)裝飾器:條件為真時,跳過裝飾的測試,并說明跳過測試的原因。
@unittest.skipUnless(reason):skipUnless(condition,reason)裝飾器:條件為假時,跳過裝飾的測試,并說明跳過測試的原因。
@unittest.expectedFailure():expectedFailure()測試标記為失敗。
setUp():setUp()方法用于每個測試用例執行前的初始化工作。如測試用例中需要通路資料庫,可以在setUp中建立資料庫連接配接并進行初始化。如測試用例需要登入web,可以先執行個體化浏覽器。
tearDown():tearDown()方法用于每個測試用例執行之後的善後工作。如關閉資料庫連接配接、關閉浏覽器。
setUpClass():setUpClass()方法用于所有測試用例前的設定工作。
tearDownClass():tearDownClass()方法用于所有測試用例執行後的清理工作。
addTest():addTest()方法是将測試用例添加到測試套件中。
run():run()方法是運作測試套件的測試用例,入參為suite測試套件。
assert*():在執行測試用例的過程中,最終用例是否執行通過,是通過判斷測試得到的實際結果和預期結果是否相等決定的。
斷言方式如下:
assertEqual(a,b,[msg='測試失敗時列印的資訊']):斷言a和b是否相等,相等則測試用例通過。
assertNotEqual(a,b,[msg='測試失敗時列印的資訊']):斷言a和b是否相等,不相等則測試用例通過。
assertTrue(x,[msg='測試失敗時列印的資訊']):斷言x是否True,是True則測試用例通過。
assertFalse(x,[msg='測試失敗時列印的資訊']):斷言x是否False,是False則測試用例通過。
assertIs(a,b,[msg='測試失敗時列印的資訊']):斷言a是否是b,是則測試用例通過。
assertNotIs(a,b,[msg='測試失敗時列印的資訊']):斷言a是否是b,不是則測試用例通過。
assertIsNone(x,[msg='測試失敗時列印的資訊']):斷言x是否None,是None則測試用例通過。
assertIsNotNone(x,[msg='測試失敗時列印的資訊']):斷言x是否None,不是None則測試用例通過。
assertIn(a,b,[msg='測試失敗時列印的資訊']):斷言a是否在b中,在b中則測試用例通過。
assertNotIn(a,b,[msg='測試失敗時列印的資訊']):斷言a是否在b中,不在b中則測試用例通過。
assertIsInstance(a,b,[msg='測試失敗時列印的資訊']):斷言a是是b的一個執行個體,是則測試用例通過。
assertNotIsInstance(a,b,[msg='測試失敗時列印的資訊']):斷言a是是b的一個執行個體,不是則測試用例通過。
說了這麼多的屬性,接下來用一段代碼舉例:
unittest基本代碼示例:
import unittest
from unittestbasic1.unittest_operation import *
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_basic.py
@time: 2019-09-26 23:38
@desc:
'''
class TestMathFunc(unittest.TestCase):
# TestCase基類方法,所有case執行之前自動執行
@classmethod
def setUpClass(cls):
print("*************這裡是所有測試用例前的準備工作*************")
# TestCase基類方法,所有case執行之後自動執行
@classmethod
def tearDownClass(cls):
print("*************這裡是所有測試用例後的清理工作*************")
# TestCase基類方法,每次執行case前自動執行
def setUp(self):
print("-------------這裡是一個測試用例前的準備工作-------------")
# TestCase基類方法,每次執行case後自動執行
def tearDown(self):
print("-------------這裡是一個測試用例後的清理工作-------------")
@unittest.skip("我想臨時跳過這個測試用例.")
def test_add(self):
self.assertEqual(3, add(1, 2))
self.assertNotEqual(3, add(2, 2)) # 測試業務方法add
def test_minus(self):
self.skipTest('跳過這個測試用例')
self.assertEqual(1, minus(3, 2)) # 測試業務方法minus
def test_multi(self):
self.assertEqual(6, multi(2, 3)) # 測試業務方法multi
def test_divide(self):
self.assertEqual(2, divide(6, 3)) # 測試業務方法divide
self.assertEqual(2.5, divide(5, 2))
if __name__ == '__main__':
unittest.main(verbosity=2)
基本函數如下:
def add(a, b):
return a+b
def minus(a, b):
return a-b
def multi(a, b):
return a*b
def divide(a, b):
return a/b
運作代碼,結果如下所示:
理論加實踐,結合起來看,應該更容易明白些,切莫做紙上談兵。
添加用例運作方式
弄明白了unittest的基本屬性,接下來分享下添加用例運作的不同方式
方式一:使用addTests單個添加
用addTests方法單個添加用例,代碼示例如下:
import unittest
from unittestbasic2.unittest_operation2 import TestMathFunc
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite1.py
@time: 2019-09-26 23:38
@desc:
'''
if __name__ == '__main__':
suite = unittest.TestSuite()
# 單個添加測試用例
suite.addTest(TestMathFunc("test_multi"))
suite.addTest(TestMathFunc("test_divide"))
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
方式二:測試用例添加到用例集中
從方法一中看,單個添加用例比較麻煩,該方法可以将用例添加到一個用例集中,再通過suite統一運作,代碼示例如下:
import unittest
from unittestbasic2.unittest_operation2 import TestMathFunc
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite2.py
@time: 2019-09-26 23:38
@desc:
'''
if __name__ == '__main__':
suite = unittest.TestSuite()
# 将測試用例添加到一個用例集中
tests = [TestMathFunc("test_add"), TestMathFunc("test_minus")]
suite.addTests(tests) # 将測試用例清單添加到測試組中
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
方式三:TestLoader方法加載用例
使用TestLoader方法加載用例,就是無法對case進行排序,執行順序是随機的。實作方法有如下三種,代碼示例如下:
import unittest
from unittestbasic2.unittest_operation2 import TestMathFunc
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite3.py
@time: 2019-09-26 23:44
@desc:
'''
if __name__ == '__main__':
suite = unittest.TestSuite()
# 第一種方法:傳入'子產品名.TestCase名'
# 用addTests + TestLoader。不過用TestLoader的方法是無法對case進行排序的
# loadTestsFromName(),傳入'子產品名.TestCase名'
# ①
# suite.addTests(unittest.TestLoader().loadTestsFromName('unittest_operation2.TestMathFunc'))
# suite.addTests(unittest.TestLoader().loadTestsFromName('unittest_operation1.TestMathFunc'))
# 這裡還可以把'子產品名.TestCase名'放到一個清單中
# ②
# suite.addTests(unittest.TestLoader().loadTestsFromNames(['unittest_operation2.TestMathFunc', 'unittest_operation1.TestMathFunc']))
# 第二種方法:傳入TestCase 需要導入對應子產品
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
方式四:通過discover()方法
看了前三種實作方式,都比較單一,實際運用中,case數量不僅僅隻有一兩個,一個一個去添加的話,那得多麻煩。故而引用discover()方法,比對指定檔案夾下以test開頭的測試用例,代碼示例如下:
import unittest
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite4.py
@time: 2019-09-26 23:46
@desc:
'''
if __name__ == '__main__':
path = './testcase'
all_cases = unittest.defaultTestLoader.discover(path, 'test*.py')
# 找到某個目錄下所有的以test開頭的Python檔案裡面的測試用例
runner = unittest.TextTestRunner(verbosity=2)
runner.run(all_cases)
如上所述,就是常見的用例執行方式了,不過,運用最多的還是最後一種方式。
測試報告
在做測試時,自然是要生成測試報告的,一份好的測試報告,可以很直覺的看出目前代碼建構的系統狀況,也可以幫助自己很快定位問題。
前段時間,在github上搜尋到一份帶截圖的測試報告模闆,是以引用使用了一番,整體很不錯,自己也略小有調整,将報告模闆引用到代碼中即可使用。
說到測試報告,再使用之前的代碼,就實作不了,需要引用HTMLTestRunner。我一般使用html格式的報告,還有一種是xml格式的,使用jenkins建構代碼的話,那就需要生成xml格式的測試報告了。
html格式測試報告
先來看如何生成html格式的測試報告,代碼示例如下:
import unittest
from utils.HTMLTestRunner_cn import HTMLTestRunner
import time
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite5.py
@time: 2019-09-26 23:58
@desc:
'''
if __name__ == '__main__':
path = './testcase'
report_file = '../'
# 找到某個目錄下所有的以test開頭的Python檔案裡面的測試用例
all_cases = unittest.defaultTestLoader.discover(path, 'test*.py')
report_time = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
report_file_path = report_file + '/report/' + report_time + '-result.html'
file_result = open(report_file_path, 'wb')
runner = HTMLTestRunner(stream=file_result, title="簡單運算demo", description="溫一壺清酒", verbosity=2, retry=2,
save_last_try=True)
runner.run(all_cases)
運作後,檢視測試報告,如下所示:
以自己的産品做了個登入測試,生成的測試報告更全面,代碼示例如下:
from selenium import webdriver
import unittest
import time
from utils.HTMLTestRunner_cn import HTMLTestRunner
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittestlogin.py
@time: 2019-09-22 11:38
@desc:
'''
class case_01(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome()
cls.driver.maximize_window()
@classmethod
def tearDownClass(cls):
cls.driver.quit()
def add_img(self):
self.imgs.append(self.driver.get_screenshot_as_base64())
return True
def setUp(self):
# 在是python3.x 中,如果在這裡初始化driver ,因為3.x版本 unittestbasic1 運作機制不同,會導緻用力失敗時截圖失敗
self.driver.get("")
self.driver.implicitly_wait(30)
self.imgs = []
self.addCleanup(self.cleanup)
def cleanup(self):
pass
def test_case1(self):
""" 正常登入"""
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
self.add_img()
self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
time.sleep(1)
self.add_img()
self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
time.sleep(1)
self.add_img()
self.driver.find_element_by_css_selector("button[type='button']").click()
time.sleep(3)
self.add_img()
user_name = self.driver.find_element_by_css_selector(".user-name").text
self.assertEqual("一壺清酒stage1111", user_name, msg="登入失敗!!")
self.driver.find_element_by_css_selector(".user-name").click()
time.sleep(3)
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[2]/div[1]/div[2]/div[5]/div[3]/ul/li[2]/span/i").click()
print("登出")
time.sleep(5)
def test_case2(self):
""" 賬号不存在"""
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
time.sleep(1)
self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
time.sleep(1)
self.driver.find_element_by_css_selector("button[type='button']").click()
time.sleep(1)
self.add_img()
print("登入失敗!")
def test_case3(self):
""" 密碼錯誤"""
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
time.sleep(1)
self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
time.sleep(1)
self.driver.find_element_by_css_selector("button[type='button']").click()
time.sleep(1)
self.add_img()
print("登入失敗!")
@unittest.skip("臨時跳過這個測試用例")
def test_case4(self):
""" 跳過該用例"""
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
self.driver.find_element_by_css_selector("input[type='text']").text()
time.sleep(1)
self.driver.find_element_by_css_selector("input[type='password']").text()
time.sleep(1)
self.driver.find_element_by_css_selector("button[type='button']").click()
time.sleep(3)
@unittest.skipIf(3 > 2, "判斷為真,此用例不執行")
def test_case5(self):
""" skipIf判斷,為真則不執行"""
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
self.driver.find_element_by_css_selector("input[type='text']").text()
time.sleep(1)
self.driver.find_element_by_css_selector("input[type='password']").text()
time.sleep(1)
self.driver.find_element_by_css_selector("button[type='button']").click()
time.sleep(3)
@unittest.skipUnless(3 < 2, "判斷為假,此用例不執行")
def test_case6(self):
""" skipIf判斷,為假則不執行"""
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
self.driver.find_element_by_css_selector("input[type='text']").text()
time.sleep(1)
self.driver.find_element_by_css_selector("input[type='password']").text()
time.sleep(1)
self.driver.find_element_by_css_selector("button[type='button']").click()
time.sleep(3)
def test_case7(self):
""" 斷言失敗"""
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
time.sleep(1)
self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
time.sleep(1)
self.driver.find_element_by_css_selector("button[type='button']").click()
time.sleep(3)
user_name = self.driver.find_element_by_css_selector(".user-name").text
self.assertEqual("一壺清酒stage", user_name, msg="登入失敗!!")
self.driver.find_element_by_css_selector(".user-name").click()
time.sleep(3)
self.driver.find_element_by_xpath(
"//*[@id='app']/div/div[2]/div[1]/div[2]/div[5]/div[3]/ul/li[2]/span/i").click()
print("登出")
if __name__ == "__main__":
suites = unittest.TestSuite()
suites.addTest(case_01("test_case1"))
suites.addTest(case_01("test_case2"))
suites.addTest(case_01("test_case3"))
suites.addTest(case_01("test_case4"))
suites.addTest(case_01("test_case5"))
suites.addTest(case_01("test_case6"))
suites.addTest(case_01("test_case7"))
report_time = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
report_file = '../'
report_file_path = report_file + '/report/' + report_time + '-result.html'
file_result = open(report_file_path, 'wb')
# retry重試次數
runer = HTMLTestRunner(stream=file_result, title="帶截圖的測試報告", description="溫一壺清酒", verbosity=2, retry=0,
save_last_try=True)
runer.run(suites)
檢視測試報告如下所示:
xml格式測試報告
生成xml格式的測試報告,代碼示例如下:
import unittest
import time
import xmlrunner
'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite6.py
@time: 2019-09-27 00:11
@desc:
'''
if __name__ == '__main__':
path = './testcase'
report_file = '../'
# 找到某個目錄下所有的以test開頭的Python檔案裡面的測試用例
all_cases = unittest.defaultTestLoader.discover(path, 'test*.py')
report_time = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
report_file_path = report_file + '/report/'
runner = xmlrunner.XMLTestRunner(verbosity=2, output=report_file_path) # output 報告存放路徑
runner.run(all_cases)
生成的報告如下所示:
問題
生成的測試報告,發送到郵箱中,預覽附件時,部分文字是亂碼,樣式按鈕也不可點選,是因為樣式檔案被浏覽器攔截了嘛?
預覽檔案如下: