unittest測試框架詳解

  • 2020 年 3 月 16 日
  • 筆記

單元測試的定義

1. 什麼是單元測試?

​ 單元測試是指,對軟件中的最小可測試單元在與程序其他部分相隔離的情況下進行檢查和驗證的工作,這裡的最小可測試單元通常是指函數或者類,一般是開發來做的,按照測試階段來分,就是單元測試、集成測試、系統測試以及驗收測試。

2.為什麼要做單元測試?

  • ? 單元測試之後,才是集成測試,單個單個的功能模塊測試通過之後,才能把單個功能模塊集成起來做集成測試,為了從底層發現bug,單元測試時可以減少合成後出現的問題。
  • ? 越早發現bug越好,這樣可以早點發現問題,不然問題累計到後面,很可能會因為一個做錯了而導致整個模塊甚至更大範圍的推倒重來,對於時間和經費來說,是非常浪費的!
  • ? 對於測試來說,單元測試就是為了執行用例,輸入測試數據–》輸出測試結果

unittest框架及原理

​ 做過自動化測試的同學應該都知道python中的unittest框架,它是python自帶的一套測試框架,學習起來也相對較容易,unittest框架最核心的四個概念:

​ ? test case:就是我們的測試用例,unittest中提供了一個基本類TestCase,可以用來創建新的測試用例,一個TestCase的實例就是一個測試用例;unittest中測試用例方法都是以test開頭的,且執行順序會按照方法名的ASCII值排序。

​ ? test fixure:測試夾具,用於測試用例環境的搭建和銷毀。即用例測試前準備環境的搭建(SetUp前置條件),測試後環境的還原(TearDown後置條件),比如測試前需要登錄獲取token等就是測試用例需要的環境,運行完後執行下一個用例前需要還原環境,以免影響下一條用例的測試結果。

​ ? test suite:測試套件,用來把需要一起執行的測試用例集中放到一塊執行,相當於一個籃子。我們可以使用TestLoader來加載測試用例到測試套件中。

​ ? test runner:用來執行測試用例的,並返回測試用例的執行結果。它還可以用圖形或者文本接口,把返回的測試結果更形象的展現出來,如:HTMLTestRunner。

unittest的斷言

​ 在python基礎中,我們有講過一個assert斷言,使用方法比較簡單,即assert 表達式, 提示信息,而unittest框架中也提供了一個自帶的斷言方式,主要有以下幾種:

方法 檢查
assertEqual(a, b,msg=None) a ==b
assertNotEqual(a, b) a !=b
assertTrue(x) bool(x) is True
assertFalse(x) Bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a,b)
assertNotIsInstance(a, b) not isinstance(a,b)

​ 如果斷言失敗即不通過就會拋出一個AssertionError斷言錯誤,成功則標識為通過,以上幾種方式都有一個共同點,就是都有一個msg參數(表中只列了一個,其實都有),默認是None,即msg = None,如果指定msg參數的值,則將該信息作為失敗的錯誤信息返回。

TestCase測試用例

編寫測試用例前,我們需要建一個測試類繼承unittest裏面的TestCase類,繼承這個類之後我們才是真正的使用unittest框架去寫測試用例,編寫測試用例的步驟如下:

  • 導入unittest模塊
  • 創建一個測試類,並繼承unittest.TestCase()
  • 定義測試方法,方法名必須以test_開頭
  • 調用unittest.main()方法來運行測試用例,unittest.main()方法會搜索該模塊下所有以test開頭的測試用例方法,並自動執行

下面以註冊功能為例,這個register.py就是註冊功能的代碼,沒有前端界面,功能比較簡單,只是方便用於演示,直接導入就可以使用。

# register.py  users = [{'username': 'test', 'password': '123456'}]      def register(username, password1, password2):        if not all([username, password1, password2]):          return {"code": 0, "msg": "所有參數不能為空"}      # 註冊功能      for user in users:          if username == user['username']:              return {"code": 0, "msg": "該用戶名已存在!"}      else:          if password1 != password2:              return {"code": 0, "msg": "兩次密碼輸入不一致!"}          else:              if 6 <= len(username) >= 6 and 6 <= len(password1) <= 18:                  users.append({'username': username, 'password': password2})                  return {"code": 1, "msg": "註冊成功"}              else:                  return {"code": 0, "msg": "用戶名和密碼必須在6-18位之間"}

​ 下面是編寫測試用例例子:

# test_register.py  import unittest  from register import register   # 導入被測試的代碼      class TestRegister(unittest.TestCase):      """註冊接口測試用例類"""        def test_register_success(self):          """註冊成功"""          data = ("mikitest", "miki123", "miki123")   # 測試數據          expected = {"code": 1, "msg": "註冊成功!"}   # 預期結果          result = register(*data)    # 把測試數據傳到被測的代碼,接收實際結果          self.assertEqual(expected, result)  # 斷言,預期和實際是否一致,一致即用例通過        def test_username_isnull(self):          """註冊失敗-用戶名為空"""          data = ("", "miki123", "miki123")          expected = {"code": 0, "msg": "所有參數不能為空!"}          result = register(*data)          self.assertEqual(expected, result)        def test_username_lt6(self):          """註冊失敗-用戶名大於18位"""          data = ("mikitestmikitestmikitest", "miki123", "miki123")          expected = {"code": 0, "msg": "用戶名和密碼必須在6-18位之間!"}          result = register(*data)          self.assertEqual(expected, result)  # 這條用例應該是不通過的,註冊代碼bug        def test_pwd1_not_pwd2(self):          """註冊失敗-兩次密碼不一致"""          data = ("miki123", "test123", "test321")          expected = {"code": 0, "msg": "兩次密碼輸入不一致!"}          result = register(*data)          self.assertEqual(expected, result)      # 如果直接運行這個文件,需要使用unittest中的main函數來執行測試用例  if __name__ == '__main__':      unittest.main()

​ 上面傳遞測試數據處用到一個*解包,我在python基礎中有講過解包的原理和例子,不明白的可以往回翻看一下,傳送門:https://www.cnblogs.com/miki-peng/p/12229199.html

​ 測試用例運行結果如下,一共4條用例,其中通過3條,不通過1條,不通過的是本身註冊代碼的bug。

Testing started at 21:58 ...  C:softwarepythonpython.exe "C:Program FilesJetBrainsPyCharm Community Edition 2019.1.3helperspycharm_jb_unittest_runner.py" --target test_register.TestRegister  Launching unittests with arguments python -m unittest test_register.TestRegister in D:learnpython_test      {'code': 1, 'msg': '註冊成功!'} != {'code': 0, 'msg': '用戶名和密碼必須在6-18位之間!'}    Expected :{'code': 0, 'msg': '用戶名和密碼必須在6-18位之間!'}  Actual   :{'code': 1, 'msg': '註冊成功!'}  <Click to see difference>    Traceback (most recent call last):    File "C:Program FilesJetBrainsPyCharm Community Edition 2019.1.3helperspycharmteamcitydiff_tools.py", line 32, in _patched_equals      old(self, first, second, msg)    File "C:softwarepythonlibunittestcase.py", line 839, in assertEqual      assertion_func(first, second, msg=msg)    File "C:softwarepythonlibunittestcase.py", line 1138, in assertDictEqual      self.fail(self._formatMessage(msg, standardMsg))    File "C:softwarepythonlibunittestcase.py", line 680, in fail      raise self.failureException(msg)  AssertionError: {'code': 0, 'msg': '用戶名和密碼必須在6-18位之間!'} != {'code': 1, 'msg': '註冊成功!'}  - {'code': 0, 'msg': '用戶名和密碼必須在6-18位之間!'}  + {'code': 1, 'msg': '註冊成功!'}    During handling of the above exception, another exception occurred:    Traceback (most recent call last):    File "C:softwarepythonlibunittestcase.py", line 59, in testPartExecutor      yield    File "C:softwarepythonlibunittestcase.py", line 615, in run      testMethod()    File "D:learnpython24python_baseday13_tasktest_register.py", line 36, in test_username_lt6      self.assertEqual(expected, result)      Ran 4 tests in 0.007s    FAILED (failures=1)    Process finished with exit code 1    Assertion failed

TestFixure測試夾具

​ unittest的測試夾具有兩種使用方式,一種是以測試方法為維度的setUp()tearDown(),一種是以測試類為維度的setUpClass()tearDownClass()。以註冊功能為例,但這個註冊代碼比較簡單,沒有真正需要用到測試夾具的地方,因此這只是個用法演示。

# test_register.py  import unittest  from register import register   # 導入被測試的代碼      class TestRegister(unittest.TestCase):      """註冊接口測試用例類"""        def setUp(self):          # 每條用例執行之前都會執行          print("用例{}開始執行--".format(self))        def tearDown(self):          # 每條用例執行之後都會執行          print("用例{}執行結束--".format(self))        @classmethod    # 指明這是個類方法以類為維度去執行的      def setUpClass(cls):          # 整個測試用例類中的用例執行之前,會先執行此方法          print("-----setup---class-----")        @classmethod      def tearDownClass(cls):          # 整個測試用例類中的用例執行完之後,會執行此方法          print("-----teardown---class-----")        def test_register_success(self):          """註冊成功"""          data = ("mikitest", "miki123", "miki123")   # 測試數據          expected = {"code": 1, "msg": "註冊成功!"}   # 預期結果          result = register(*data)    # 把測試數據傳到被測的代碼,接收實際結果          self.assertEqual(expected, result)  # 斷言,預期和實際是否一致,一致即用例通過        def test_username_isnull(self):          """註冊失敗-用戶名為空"""          data = ("", "miki123", "miki123")          expected = {"code": 0, "msg": "所有參數不能為空!"}          result = register(*data)          self.assertEqual(expected, result)      # 如果直接運行這個文件,需要使用unittest中的main函數來執行測試用例  if __name__ == '__main__':      unittest.main()

​ 運行結果:

Testing started at 22:19 ...  C:softwarepythonpython.exe "C:Program FilesJetBrainsPyCharm Community Edition 2019.1.3helperspycharm_jb_unittest_runner.py" --path D:/learn/python/test_register.py  Launching unittests with arguments python -m unittest D:/learn/python/test_register.py in D:learnpython  -----setup---class-----用例test_pwd1_not_pwd2 (test_register.RegisterTestCase)開始執行--  用例test_pwd1_not_pwd2 (test_register.RegisterTestCase)執行結束--  用例test_register_success (test_register.RegisterTestCase)開始執行--  用例test_register_success (test_register.RegisterTestCase)執行結束--  用例test_username_isnull (test_register.RegisterTestCase)開始執行--  用例test_username_isnull (test_register.RegisterTestCase)執行結束--  用例test_username_lt6 (test_register.RegisterTestCase)開始執行--  用例test_username_lt6 (test_register.RegisterTestCase)執行結束--  -----teardown---class-----    Ran 4 tests in 0.003s    OK    Process finished with exit code 0

TestSuite測試套件

​ unittest.TestSuite()類來表示一個測試用例集,把需要執行的用例類或模塊存到一起,常用的方法如下:

  • ? unittest.TestSuite()
    • addTest():添加單個測試用例方法
    • addTest([..]):添加多個測試用例方法,方法名存在一個列表
  • ? unittest.TestLoader()
    • loadTestsFromTestCase(測試類名):添加一個測試類
    • loadTestsFromModule(模塊名):添加一個模塊
    • discover(測試用例的所在目錄):指定目錄去加載,會自動尋找這個目錄下所有符合命名規則的測試用例
# run_test.py,與test_register.py、register.py同一目錄下  import unittest  import test_register    # 第一步,創建一個測試套件  suite = unittest.TestSuite()    # 第二步:將測試用例,加載到測試套件中  # 方式1,添加單條測試用例  # case = test_register("test_register_success") # 創建一個用例對象,注意:通過用例類去創建測試用例對象的時候,需要傳入用例的方法名(字符串類型)  # suite.addTest(case)   # 添加用例到測試套件中    # 方式2,添加多條測試用例  # case1 = test_register("test_register_success")  # case2 = test_register("test_username_isnull")  # suite.addTest([case1, case2]) # 添加用例到測試套件中    # 方式3,添加一個測試用例類  # loader = unittest.TestLoader()    # 創建一個加載對象  # suite.addTest(loader.loadTestsFromTestCase(test_register.TestRegister))    # 方式4,添加一個模塊  loader = unittest.TestLoader()  # 創建一個加載對象  suite.addTest(loader.loadTestsFromModule(test_register))    # 方式5,指定測試用例的所在的目錄路徑,進行加載  # loader = unittest.TestLoader()  # suite.addTest(loader.discover(r"d:learnpython"))

​ 通常我們使用方式4、5比較多,你可以根據實際情況來運用。其中方式5,還可以自定義匹配規則,默認是會尋找目錄下test*.py文件,即所有以test開頭命名的py文件,自定義如下:

loader = unittest.TestLoader()  suite.addTest(loader.discover(start_dir = r"d:learnpython", pattern="test_case*.py"))     # 匹配規則:所有以test_case開頭的

TestRunner執行用例

​ test runner顧名思義就是用來執行測試用例的,並且可以生成相應的測試報告。測試報告有兩種展示形式,一種是text文本,一種是html格式。

​ html格式的就是HTMLTestRunner了,HTMLTestRunner是 Python 標準庫的 unittest 框架的一個擴展,它可以生成一個直觀清晰的 HTML 測試報告。使用的前提就是要下載 HTMLTestRunner.py,下載完後放在python的安裝目錄下的scripts目錄下即可。

​ text文本相對於html來說過於簡陋,與控制台輸出的沒有什麼區別,也幾乎沒有人使用,這裡不作演示,使用方法是一樣的。我們結合前面的測試套件來演示一下如何生成html格式的測試報告:

# run_test.py,與test_register.py、register.py同一目錄下  import unittest  import test_register  from HTMLTestRunner import HTMLTestRunner    # 創建測試套件  suite = unittest.TestSuite()    # 通過模塊加載測試用例  loader = unittest.TestLoader()  suite.addTest(loader.loadTestsFromModule(test_register))    # 創建測試運行程序啟動器  runner = HTMLTestRunner(stream=open("report.html", "wb"),  # 打開一個報告文件,將句柄傳給stream                          tester="miki",                    # 報告中顯示的測試人員                          description="註冊接口測試報告",        # 報告中顯示的描述信息                          title="自動化測試報告")                 # 報告的標題    # 使用啟動器去執行測試套件里的用例  runner.run(suite)

相關參數說明:

  • stream:指定輸出的方式
  • tester:報告中要顯示的測試人員的名字
  • description:報告中要顯示的面熟信息
  • title:測試報告的標題
  • verbosity :表示測試報告信息的詳細程度,一共三個值,默認是2
    • 0 (靜默模式):你只能獲得總的測試用例數和總的結果,如:總共100個 失敗10 成功90
    • 1 (默認模式):類似靜默模式,只是在每個成功的用例前面有個. 每個失敗的用例前面有個F
    • 2 (詳細模式):測試結果會顯示每個測試用例的所有相關的信息

​ 運行完畢,你會發現你的項目目錄下已經生成了一個report.html文件,在瀏覽器中打開,就可以查看測試報告了。

image-20200315232546384