unittest是Python標準庫自帶的單元測試框架,是Python版本的JUnit,關於unittest框架的使用,官方文檔很是詳細,網上也有很多好的教程,這裏就很少說了。html
本文主要分享在使用unittest的過程當中,作的一些擴展嘗試。先上一個例子。python
import unittest class TestLegion(unittest.TestCase): def test_create_legion(self): """建立軍團 :return: """ def test_bless(self): """ 公會祈福 :return: """ def test_receive_bless_box(self): """ 領取祈福寶箱 :return: """ def test_quit_legion(self): """退出軍團 :return: """
這是一個標準的使用unittest進行測試的例子,寫完後內心美滋滋,嗯,就按照這個順序測就能夠了。結果一運行。git
什麼鬼。執行的順序亂了。第一個執行的測試用例並非建立軍團,而是公會祈福,此時玩家還沒建立軍團,進行公會祈福的話會直接報錯,致使用例失敗。github
到這裏有些同窗會想說,爲何要讓測試用例之間有所依賴呢?app
的確,若是徹底沒依賴,測試用例的執行順序是不須要關注的。可是這樣對於用例的設計和實現,要求就高了許多。而對遊戲來講,一個系統內的操做,是有很大的關聯性的。以軍團爲例,軍團內的每一個操做都有一個前提,你須要加入一個軍團。因此要實現用例之間的徹底解耦,須要每一個用例開始以前,檢測玩家的軍團狀態。框架
若是能夠控制測試用例的執行順序,按照功能玩法流程一遍走下來,節省的代碼量是很是可觀的,閱讀測試用例也會清晰許多。less
咱們先看看,unittest是怎麼樣對用例進行排序的。在loader.py
的loadTestsFromTestCase
方法裏邊,調用了getTestCaseNames
方法來獲取測試用例的名稱函數
def getTestCaseNames(self, testCaseClass): """Return a sorted sequence of method names found within testCaseClass """ def isTestMethod(attrname, testCaseClass=testCaseClass, prefix=self.testMethodPrefix): return attrname.startswith(prefix) and \ callable(getattr(testCaseClass, attrname)) testFnNames = list(filter(isTestMethod, dir(testCaseClass))) if self.sortTestMethodsUsing: testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing)) return testFnNames
能夠看到,getTestCaseNames
方法對測試用例的名稱進行了排序工具
testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
看看排序方法單元測試
def three_way_cmp(x, y): """Return -1 if x < y, 0 if x == y and 1 if x > y""" return (x > y) - (x < y)
根據排序規則,unittest執行測試用例,默認是根據ASCII碼的順序加載測試用例,數字與字母的順序爲:0-9,A-Z,a-z。
作個實驗:
import functools case_names = ["test_buy_goods", "test_Battle", "test_apply", "test_1_apply"] def three_way_cmp(x, y): """Return -1 if x < y, 0 if x == y and 1 if x > y""" return (x > y) - (x < y) case_names.sort(key=functools.cmp_to_key(three_way_cmp)) print(case_names) output:['test_1_apply', 'test_Battle', 'test_apply', 'test_buy_goods']
基於unittest的機制,如何控制用例執行順序呢?查了一些網上的資料,主要介紹了兩種方式:
suite = unittest.TestSuite()
suite.addTest(TestLegion("test_create_legion")) suite.addTest(TestLegion("test_bless")) suite.addTest(TestLegion("test_receive_bless_box")) suite.addTest(TestLegion("test_quit_legion")) unittest.TextTestRunner(verbosity=3).run(suite)
class TestLegion(unittest.TestCase): def test_1_create_legion(self): """建立軍團 :return: """ def test_2_bless(self): """ 公會祈福 :return: """ def test_3_receive_bless_box(self): """ 領取祈福寶箱 :return: """ def test_4_quit_legion(self): """退出軍團 :return: """
看起來都能知足需求,可是都不夠好用,繁瑣,代碼很差維護。
因而開始了utx這個小項目,那麼如何在不改動代碼的狀況下,讓測試用例按照編寫的順序依次執行呢?
方案就是,在測試類初始化的時候,將測試方法按照編寫的順序,自動依次重命名爲「test_1_create_legion」,「test_2_bless」,「test_3_receive_bless_box」等等,從而實現控制測試用例的執行。
這就須要控制類的建立行爲,Python提供了一個很是強力的工具:元類,在元類的__new__
方法中,咱們能夠獲取類的所有成員函數,另外基於Python3.6的字典底層重構後,字典是有序的了,默認順序和添加的順序一致。因此咱們拿到的測試用例,就和編寫的順序一致了。
接下來,就是按照順序,依次更名了,定義一個全局的total_case_num
變量,每次進行更名的時候,total_case_num
遞增+1,做爲用例的id,加入到用例的名字當中。
@staticmethod def modify_raw_func_name_to_sort_case(raw_func_name, raw_func): case_id = Tool.general_case_id() setattr(raw_func, CASE_ID_FLAG, case_id) if setting.sort_case: func_name = raw_func_name.replace("test_", "test_{:05d}_".format(case_id)) else: func_name = raw_func_name return func_name
接下來是定義本身的TestCase類,繼承unittest.TestCase
,使用上邊定義的元類
class _TestCase(unittest.TestCase, metaclass=Meta): def shortDescription(self): """覆蓋父類的方法,獲取函數的註釋 :return: """ doc = self._testMethodDoc doc = doc and doc.split()[0].strip() or None return doc
最後一步,對unittest打一個猴子補丁,將unittest.TestCase
替換爲自定義的_TestCase
unittest.TestCase = _TestCase
看下運行效果,代碼和本文開始的例子同樣,只是多了一句utx庫的導入。
import unittest from utx import * class TestLegion(unittest.TestCase): def test_create_legion(self): """建立軍團 :return: """ def test_bless(self): """ 公會祈福 :return: """ def test_receive_bless_box(self): """ 領取祈福寶箱 :return: """ def test_quit_legion(self): """退出軍團 :return: """
運行效果:
執行順序就和咱們的預期一致了~
基於這一套,開始加上其餘的一些擴展功能,好比
@unique class Tag(Enum): SMOKE = 1 # 冒煙測試標記,能夠重命名,可是不要刪除 FULL = 1000 # 完整測試標記,能夠重命名,可是不要刪除 # 如下開始爲擴展標籤,自行調整 SP = 2
class TestLegion(unittest.TestCase): @tag(Tag.SMOKE) def test_create_legion(self): pass @tag(Tag.SP, Tag.FULL) def test_quit_legion(self): """退出軍團 :return: """ print("吧啦啦啦啦啦啦") assert 1 == 2
from utx import * if __name__ == '__main__': setting.run_case = {Tag.SMOKE} # 只運行SMOKE冒煙用例 # setting.run_case = {Tag.FULL} # 運行所有測試用例 # setting.run_case = {Tag.SMOKE, Tag.SP} # 只運行標記爲SMOKE和SP的用例 runner = TestRunner() runner.add_case_dir(r"testcase") runner.run_test(report_title='接口自動化測試報告')
class TestLegion(unittest.TestCase): @data(["gold", 100], ["diamond", 500]) def test_bless(self, bless_type, award): print(bless_type) print(award) @data(10001, 10002, 10003) def test_receive_bless_box(self, box_id): """ 領取祈福寶箱 :return: """ print(box_id) # 默認會解包測試數據來一一對應函數參數,可使用unpack=False,不進行解包 class TestBattle(unittest.TestCase): @data({"gold": 1000, "diamond": 100}, {"gold": 2000, "diamond": 200}, unpack=False) def test_get_battle_reward(self, reward): """ 領取戰鬥獎勵 :return: """ print(reward) print("得到的鑽石數量是:{}".format(reward['diamond']))
2017-11-03 12:00:19,334 WARNING legion.test_legion.test_bless沒有用例描述
2017-11-03 12:00:19,336 INFO 開始進行測試 2017-11-03 12:00:19,436 INFO Start to test legion.test_legion.test_create_legion (1/5) 2017-11-03 12:00:19,536 INFO Start to test legion.test_legion.test_receive_bless_box (2/5) 2017-11-03 12:00:19,637 INFO Start to test legion.test_legion.test_receive_bless_box (3/5) 2017-11-03 12:00:19,737 INFO Start to test legion.test_legion.test_receive_bless_box (4/5) 2017-11-03 12:00:19,837 INFO Start to test legion.test_legion.test_quit_legion (5/5)
class setting: # 只運行的用例類型 run_case = {Tag.SMOKE} # 開啓用例排序 sort_case = True # 每一個用例的執行間隔,單位是秒 execute_interval = 0.1 # 開啓檢測用例描述 check_case_doc = True # 顯示完整用例名字(函數名字+參數信息) full_case_name = False # 測試報告顯示的用例名字最大程度 max_case_name_len = 80 # 執行用例的時候,顯示報錯信息 show_error_traceback = True # 生成ztest風格的報告 create_ztest_style_report = True # 生成bstest風格的報告 create_bstest_style_report = True
utx庫核心源碼不到200行,就不作過多講解了,直接去Github看吧
做者:煎煎煎餅 連接:https://www.jianshu.com/p/d65f97723af7 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。