日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪(fǎng)問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > python >内容正文

python

万字长文深度解析python 单元测试

發(fā)布時(shí)間:2024/1/23 python 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 万字长文深度解析python 单元测试 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

  • unittest
      • 基本概念
      • 基本用法
      • 命令行操作
      • 組織測(cè)試用例
      • 跳過(guò)測(cè)試&預(yù)期錯(cuò)誤
      • subTest
      • unittest小結(jié)
  • doctest
      • 先談pydoc
      • doctest與unittest
  • Mock
      • 初步理解Mock object
      • 使用Mock object
      • 定制化Mock object
          • return_value
          • side_effect
      • 配置Mock
      • patch
          • as a Decorator
          • as a Context Manager
      • patch.object
  • 寫(xiě)在篇后

unittest

基本概念

??Python unittest模塊借鑒JUnit的思想發(fā)展而成,與其他語(yǔ)言的單元測(cè)試框架具有相似的風(fēng)格。unittest支持測(cè)試自動(dòng)化、配置共享、關(guān)機(jī)代碼測(cè)試、將測(cè)試聚合到測(cè)試集合中,以及測(cè)試與報(bào)告框架相獨(dú)立。關(guān)于測(cè)試,首先形式化的給出四個(gè)重要概念,它是unittest設(shè)計(jì)和應(yīng)用的理論指導(dǎo):

  • Test fixture表示測(cè)試執(zhí)行前所需做的準(zhǔn)備工作(測(cè)試執(zhí)行所需要的固定環(huán)境),及其相關(guān)的cleanup操作,如創(chuàng)建臨時(shí)或代理數(shù)據(jù)庫(kù)等。
  • Test case是為某個(gè)特殊目標(biāo)而編制的一組測(cè)試代碼,它檢查對(duì)特定輸入集的特定響應(yīng)。unittest提供了TestCase類(lèi),可用于創(chuàng)建測(cè)試用例。
  • Test suite是指一組test case,test suite或兩者兼有,用于聚合應(yīng)該一起執(zhí)行的測(cè)試。
  • Test runner是協(xié)調(diào)測(cè)試執(zhí)行并向用戶(hù)提供測(cè)試結(jié)果的組件。test runner可以使用圖形界面,文本界面,或返回特殊值來(lái)指示執(zhí)行測(cè)試的結(jié)果。

基本用法

??可以通過(guò)繼承unittest.TestCase類(lèi)來(lái)實(shí)現(xiàn)一個(gè)Testcase子類(lèi),用來(lái)聚合一個(gè)或多個(gè)需要相同執(zhí)行環(huán)境的測(cè)試方法。每個(gè)測(cè)試方法的定義均以test_開(kāi)頭。unittest.TestCase內(nèi)置了眾多方法用來(lái)測(cè)試輸出結(jié)果與預(yù)期結(jié)果的一致性。這里展示一個(gè)測(cè)試三種字符串方法的腳本,來(lái)體會(huì)一下unittest的基本用法:

class TestStringMethods(unittest.TestCase):def test_upper(self):self.assertEqual('foo'.upper(), 'FOO')def test_isupper(self):self.assertTrue('FOO'.isupper())self.assertFalse('Foo'.isupper())def test_split(self):s = 'hello world'self.assertEqual(s.split(), ['hello', 'world'])# check that s.split fails when the separator is not a stringwith self.assertRaises(TypeError):s.split(2)def tearDown(self) -> None:print('cleanup after testing')def setUp(self) -> None:print('preparation before testing')if __name__ == '__main__':unittest.main(module='__main__') # 輸出結(jié)果 ... ---------------------------------------------------------------------- Ran 3 tests in 0.000sOK preparation before testing cleanup after testing preparation before testing cleanup after testing preparation before testing cleanup after testing

??在上面輸出結(jié)果的第一行可以看到三個(gè)點(diǎn),這里的每個(gè)點(diǎn)都代表一個(gè)測(cè)試用例(在測(cè)試時(shí),每個(gè)以 test_ 開(kāi)頭的方法都是一個(gè)真正獨(dú)立的測(cè)試用例)的結(jié)果。由于上面測(cè)試類(lèi)中包含了三個(gè)測(cè)試用例,因此此處看到三個(gè)點(diǎn),其中點(diǎn)代表測(cè)試用例通過(guò)。此處可能出現(xiàn)如下字符:

  • .:代表測(cè)試通過(guò)。
  • F:代表測(cè)試失敗,F 代表 failure
  • E:代表測(cè)試出錯(cuò),E 代表 error
  • s:代表跳過(guò)該測(cè)試,s 代表 skip

??在上面輸出結(jié)果的橫線(xiàn)下面看到了“Ran 3 tests in 0.000s”提示信息,這行提示信息說(shuō)明本次測(cè)試運(yùn)行了多少個(gè)測(cè)試用例。如果看到下面提示 OK,則表明所有測(cè)試用例均通過(guò);setUp和tearDown方法分別在每一個(gè)test*方法執(zhí)行之前和執(zhí)行之后執(zhí)行(注意,__init__()方法也是這樣),setUp方法通常用于測(cè)試環(huán)境準(zhǔn)備,tearDown做相應(yīng)的cleanup操作。

??編寫(xiě)test case除了繼承unittest.TestCase之外,還可以使用unittest.FunctionTestCase,緊接著上面的例子,我們可以這樣寫(xiě)(但是官方不推薦使用這種,了解就好):

def test_upper():assert 'foo'.upper() == 'FOO'def tearDown() -> None:print('cleanup after testing')def setUp() -> None:print('preparation before testing')new_test_case = unittest.FunctionTestCase(testFunc=test_upper,setUp=setUp,tearDown=tearDown,description='test' )if __name__ == '__main__':runner = unittest.TextTestRunner()runner.run(new_test_case)

??下面我簡(jiǎn)單統(tǒng)計(jì)了一下unittest模塊中assert*內(nèi)置方法。方法非常多,但是只需要記住幾個(gè)常用的。其他的可以在使用到的時(shí)候再選擇一個(gè)合適的。

self.assertTrue() self.assertFalse()self.assertEqual() self.assertNotEqual() self.assertEquals() self.assertNotEquals() self.assertGreater() self.assertGreaterEqual() self.assertLess() self.assertLessEqual()self.assertAlmostEqual() self.assertNotAlmostEqual() self.assertAlmostEquals() self.assertNotAlmostEquals() self.assertListEqual() self.assertDictEqual() self.assertSequenceEqual() self.assertSetEqual() self.assertTupleEqual() self.assertCountEqual() self.assertLogs() self.assertMultiLineEqual()self.assertIn() self.assertNotIn() self.assertIs() self.assertIsNot() self.assertIsInstance() self.assertNotIsInstance() self.assertIsNone() self.assertIsNotNone()self.assertRaises() self.assertRaisesRegex() self.assertRaisesRegexp() self.assertRegex() self.assertNotRegex() self.assertRegexpMatches() self.assertWarnsRegex() self.assertWarns()self.assertLogs()

命令行操作

??上面的例子是通過(guò)unittest.main()來(lái)執(zhí)行測(cè)試,我們也可以通過(guò)命令行的方式來(lái)執(zhí)行測(cè)試。例如,上面的例子我的工程結(jié)構(gòu)如下圖所示:

??則可通過(guò)下面幾種方式運(yùn)行這些test case:

python -m unittest python -m unittest discover # 上面是此句的簡(jiǎn)寫(xiě) python -m unittest discover -s tests # 同上 python -m unittest tests.test_str python -m unittest tests.test_str.TestStringMethods python -m unittest tests.test_str.TestStringMethods.test_upperpython -m unittest tests/test_str.py python -m unittest -v tests/test_str.py # -v 表示verbose

??除了-v,unittest還有其他幾個(gè)有用的命令行參數(shù):

  • -q : quiet模式
  • -f : 遇到第一個(gè)測(cè)試失敗時(shí)停止
  • -b: 測(cè)試過(guò)程中緩存標(biāo)準(zhǔn)輸出流、標(biāo)準(zhǔn)錯(cuò)誤流
  • -c: 捕捉control-c并等待當(dāng)前正在執(zhí)行的測(cè)試完時(shí)再中斷并輸出目前為止跑完的測(cè)試的報(bào)告
  • -k: 僅運(yùn)行與給定子字符串匹配的測(cè)試,如, -k foo匹配foo_tests.SomeTest.test_something,bar_tests.SomeTest.test_foo, 但是不匹配bar_tests.FooTest.test_something
  • —locals:在tracebacks中顯示local variables

??下面幾個(gè)參數(shù)和unittest discovery有關(guān):

  • -s`: 開(kāi)始自動(dòng)搜尋測(cè)試的目錄(默認(rèn)是當(dāng)前文件夾), 該目錄必須是 importable
  • -p: 匹配測(cè)試文件的pattern(默認(rèn)為test*.py)
  • -t: 項(xiàng)目的頂級(jí)目錄,默認(rèn)是開(kāi)始搜尋的目錄
python -m unittest discover -s tests -p "*_test.py" python -m unittest discover test"*_test.py"

New in version 3.2: The command-line options -b, -c and -f were added.

New in version 3.5: The command-line option --locals.

New in version 3.7: The command-line option -k

組織測(cè)試用例

運(yùn)行各種測(cè)試的順序是通過(guò)根據(jù)字符串的內(nèi)置順序?qū)y(cè)試方法名稱(chēng)進(jìn)行排序來(lái)確定的

???對(duì)一個(gè)功能的驗(yàn)證往往是需要很多多測(cè)試用例,可以把測(cè)試用例集合在一起執(zhí)行,這就產(chǎn)生了測(cè)試套件TestSuite 的概念,它是用來(lái)組裝單個(gè)測(cè)試用例,規(guī)定用例的執(zhí)行的順序,而且TestSuite也可以嵌套TestSuite。使用Test Suite組織測(cè)試代碼一般有以下兩種使用方式:

  • suite.addTest()

    通過(guò)suite.addTest逐步添加單個(gè)測(cè)試用例, 類(lèi)似的方法還有suite.addTests通過(guò)序列添加一個(gè)多個(gè)測(cè)試用例。添加完畢之后,可以使用suite.countTestCases()計(jì)算該suite對(duì)象的測(cè)試用例數(shù)量。

import unittestclass Widget():def __init__(self, name):self.name = nameself._size = (50, 50)def size(self):return self._sizedef resize(self, tlp):self._size = tlpclass WidgetTestCase(unittest.TestCase):def setUp(self):self.widget = Widget('The widget')def test_default_widget_size(self):self.assertEqual(self.widget.size(), (50, 50),'incorrect default size')def test_widget_resize(self):self.widget.resize((100, 150))self.assertEqual(self.widget.size(), (100, 150),'wrong size after resize')def suite():suite = unittest.TestSuite()suite.addTest(WidgetTestCase('test_default_widget_size'))suite.addTest(WidgetTestCase('test_widget_resize'))return suiteif __name__ == '__main__':# runner = unittest.TextTestRunner()# runner.run(suite())unittest.main()
  • unittest.TestLoader().discover()

    可以通過(guò)TestLoader().discover()方法指定測(cè)試用例的目錄(目錄必須包含__init__.py文件),根據(jù)文件名稱(chēng)匹配測(cè)試用例。discover()是一個(gè)自動(dòng)搜索并組裝測(cè)試用例的方法,TestLoader類(lèi)還提供loadTestsFromTestCase、loadTestsFromModule、loadTestsFromName、loadTestsFromNames等方法加載Test Case。

    import unittestsuites = unittest.TestLoader().discover('./tests', pattern='test_*.py', top_level_dir=None) runner = unittest.TextTestRunner() runner.run(suites)

跳過(guò)測(cè)試&預(yù)期錯(cuò)誤

??unittest模塊支持跳過(guò)一個(gè)測(cè)試用例中的某個(gè)測(cè)試方法甚至整個(gè)測(cè)試用例。使用unittest.skip()裝飾器及其變體實(shí)現(xiàn)跳過(guò)測(cè)試,或者直接引發(fā)SkipTest異常;此外,還支持將測(cè)試標(biāo)記為預(yù)期錯(cuò)誤(expected failure),意思是該測(cè)試方法輸出結(jié)果與預(yù)期結(jié)果會(huì)不一致,但是不應(yīng)該被認(rèn)為是測(cè)試失敗。

import sys import unittestclass MyTestCase(unittest.TestCase):@unittest.skip("demonstrating skipping")def test_nothing(self):self.fail("shouldn't happen")@unittest.skipIf(sys.version_info[0]==3,"not supported in this python2 ")def test_format(self):# Tests that work for only a certain version of the library.pass@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")def test_windows_support(self):# windows specific testing codepassdef test_maybe_skipped(self):if True:self.skipTest("external resource not available")passclass ExpectedFailureTestCase(unittest.TestCase):@unittest.expectedFailuredef test_fail(self):self.assertEqual(1, 0, "broken")

被跳過(guò)的測(cè)試方法的 setUp() 和 tearDown() 不會(huì)被運(yùn)行。被跳過(guò)的類(lèi)的 setUpClass() 和 tearDownClass()不會(huì)被運(yùn)行。被跳過(guò)的模塊的 setUpModule() 和 tearDownModule() 不會(huì)被運(yùn)行。

subTest

??在python 3.4中新增了subTest()特性,可以將測(cè)試方法里循環(huán)中的每一個(gè)迭代視為一個(gè)"隱形的"測(cè)試方法,示例如下,setTest()

class NumbersTest(unittest.TestCase):def test_even(self):"""Test that numbers between 0 and 5 are all even."""for i in range(0, 6):with self.subTest(i=i):self.assertEqual(i % 2, 0)

unittest小結(jié)

??下面總結(jié)一下unittest模塊運(yùn)行單元測(cè)試的方式以及原理。unittest模塊的TestLoader類(lèi)有一個(gè)discover(self, start_dir, pattern='test*.py', top_level_dir=None)方法可以遞歸查找指定目錄(start_dir)及其子目錄下的全部測(cè)試模塊。如果一個(gè)測(cè)試模塊的名稱(chēng)符合pattern,將檢查該模塊是否包含 load_tests(loader, standard_tests, pattern) 函數(shù),如果 load_tests() 函數(shù)存在,則由該函數(shù)負(fù)責(zé)加載本模塊中的測(cè)試用例,并返回一個(gè)TestSuite對(duì)象;如果不存在,就會(huì)執(zhí)行l(wèi)oadTestsFromModule(),查找該文件中派生自TestCase 的類(lèi)包含的 test 開(kāi)頭的方法。

import unittestfrom tests import test_01, test_02 # 兩個(gè)包含TestCase派生類(lèi)的測(cè)試模塊# ---------------方式1---------------------- unittest.main(module=test_02)# ---------------方式2---------------------- runner = unittest.TextTestRunner() # suite = unittest.TestLoader().loadTestsFromModule(module=test_01) # suite = unittest.TestLoader().loadTestsFromTestCase(testCaseClass=test_01.WidgetTestCase) # suite = unittest.TestLoader().loadTestsFromName('test_default_widget_size', module=test_01.WidgetTestCase) # suite = unittest.TestLoader().loadTestsFromName('WidgetTestCase.test_default_widget_size', module=test_01) # suite = unittest.TestLoader().loadTestsFromNames(['test_default_widget_size', 'test_widget_resize'], # module=test_01.WidgetTestCase)# suite = unittest.TestSuite() # suite.addTest(test_01.WidgetTestCase('test_default_widget_size'))suite = unittest.TestLoader().discover('./tests', pattern='test_*.py', top_level_dir=None) test_result: unittest.TextTestResult = runner.run(suite)print(isinstance(test_result, (unittest.TextTestResult, unittest.TestResult)))

doctest

先談pydoc

?要說(shuō)doctest不妨先了解一下python標(biāo)準(zhǔn)庫(kù)pydoc模塊,通過(guò)pydoc模塊可以非常方便地查看、生成幫助文檔。其文檔的組織方式總是按如下順序來(lái)顯示一個(gè)模塊中的全部?jī)?nèi)容:

  • 模塊的文檔說(shuō)明:就是*.py 文件頂部的注釋信息,這部分信息會(huì)被提取成模塊的文檔說(shuō)明
  • CLASSES 部分:這部分會(huì)列出該模塊所包含的全部類(lèi)
  • FUNCTIONS 部分:這部分會(huì)列出該模塊所包含的全部函數(shù)
  • DATA 部分:這部分會(huì)列出該模塊所包含的全部成員變量
  • FILE 部分:這部分會(huì)顯示該模塊對(duì)應(yīng)的源文件

舉個(gè)例子,我寫(xiě)了一個(gè)名為tiny_example.py的模塊如下:

""" this module is for test pydoc """def say_hi(name):"""say hello to some one:param name: some one's name:return:"""print(f'hello {name}')class User:"""define a User class, including name and age"""NATIONAL = 'China'def __init__(self, name, age):"""init method, get an instance of User:param name::param age:"""self.name = nameself.age = agedef eat(self, food):"""eat method of User instance:param food: some food:return:"""print('%is eating %s' % (self.name, food))

??則可以通過(guò)python -m pydoc tiny_example.py在命令行中查看模塊文檔:

??當(dāng)然也可以為模塊生成html文件,在瀏覽器中查看文檔:

python -m pydoc tiny_example.py # 為當(dāng)前模塊生成文檔python -m pydoc directory_name # 為該文件夾下面的模塊生成文檔python3 -m pydoc -p 端口號(hào) # 啟動(dòng)本地服務(wù)器來(lái)查看文檔信息python3 -m pydoc -b # 同上python3 -m pydoc -w sys # 生成html

??現(xiàn)在回到doctest,就是在寫(xiě)代碼注釋的段落中加入測(cè)試代碼,在下面的示例中一共為該函數(shù)提供了 2 個(gè)測(cè)試用例,>>>之后的內(nèi)容表示測(cè)試用例,接下來(lái)的一行則代表該測(cè)試用例的輸出結(jié)果。寫(xiě)完之后啟動(dòng)測(cè)試也只需要簡(jiǎn)單的使用doctest.testmod()即可。

def say_hi(name):"""say hello to some onee.g.>>> say_hi("jeffery")hello jeffery>>> say_hi("barry")hi barry:param name: some one's name:return:"""print(f'hello {name}')if __name__ == '__main__':import doctestdoctest.testmod(verbose=True)

??運(yùn)行上面的代碼,測(cè)試用例say_hi("barry")將會(huì)報(bào)錯(cuò):

********************************************************************** File "/Users/jeffery/workspace/projects/Exporing/pydoctest/tiny_example.py", line 13, in __main__.say_hi Failed example:say_hi("barry") Expected:hi barry Got:hello barry ********************************************************************** 1 items had failures:1 of 2 in __main__.say_hi ***Test Failed*** 1 failures.

?每個(gè)失敗的測(cè)試用例結(jié)果都包含如下 4 部分:

  • 第一部分:顯示在哪個(gè)源文件的哪一行;
  • 第二部分:Failed example,顯示是哪個(gè)測(cè)試用例出錯(cuò)了;
  • 第三部分:Expected,顯示程序期望的輸出結(jié)果。也就是在“>>>命令”的下一行給出的運(yùn)行結(jié)果,它就是期望結(jié)果;
  • 第四部分:Got,顯示程序?qū)嶋H運(yùn)行產(chǎn)生的輸出結(jié)果。只有當(dāng)實(shí)際運(yùn)行產(chǎn)生的輸出結(jié)果與期望結(jié)果一致時(shí),才表明該測(cè)試用例通過(guò);
  • doctest與unittest

    ??doctest中的測(cè)試用例可以通過(guò)DocTestSuite來(lái)提取模塊中docstring的測(cè)試用例,并組建成TestSuite對(duì)象返回。依舊以上面的say_hi()函數(shù)為例,假設(shè)該函數(shù)所在模塊為tests.test_doctest, 則:

    import unittest import doctest from tests import test_doctestrunner = unittest.TextTestRunner() suite = unittest.TestLoader().discover('./tests', pattern='test_*.py', top_level_dir=None) s = doctest.DocTestSuite(module=test_doctest) # 提取doctest用例,返回TestSuite對(duì)象 suite.addTests(s) # 加到其他用例suite中(也可以不加,單獨(dú)run) test_result: unittest.TextTestResult = runner.run(suite)

    ??另外,doctest模塊還提供了DocFileSuite從文本文件提取測(cè)試用例,更多用法請(qǐng)參考官方文檔

    Mock

    ??Mock是Python中一個(gè)用于支持單元測(cè)試的庫(kù),它的主要功能是使用mock對(duì)象替代掉指定的Python對(duì)象,以達(dá)到模擬對(duì)象的行為。在Python 3.3及之后被整合在unittest.mock模塊中,更早的版本可以通過(guò)pip install mock進(jìn)行安裝,接下來(lái),我們就一步步探索一下Mock的使用方式。

    初步理解Mock object

    ? unittest.mock實(shí)現(xiàn)了一個(gè)Mock類(lèi),其使用非常靈活,首先實(shí)例化一個(gè)Mock對(duì)象:

    >>> from unittest.mock import Mock >>> mock = Mock() >>> mock <Mock id='4561344720'>

    ? 現(xiàn)在,你可以通過(guò)將其作為參數(shù)傳遞給函數(shù)或重新定義另一個(gè)對(duì)象來(lái)實(shí)現(xiàn)對(duì)象的替換:

    # Pass mock as an argument to do_something() do_something(mock)# Patch the json library json = mock

    ? 按道理,當(dāng)你使用mock替換代碼中的對(duì)象時(shí),它必須看起來(lái)像它正在替換的真實(shí)對(duì)象吧?否則,這豈不是一通胡亂操作?例如,如果你準(zhǔn)備 mocking json庫(kù)然后調(diào)用dumps(),那么你的mock對(duì)象也必須包含dumps()方法。為了實(shí)現(xiàn)這個(gè)功能,mock對(duì)象在你調(diào)用某一個(gè)方法屬性時(shí),創(chuàng)建這些方法和屬性(稱(chēng)為L(zhǎng)azy Attributes and Methods)。

    >>> mock.some_attribute <Mock name='mock.some_attribute' id='4394778696'> >>> mock.do_something() <Mock name='mock.do_something()' id='4394778920'>

    ? 也正是因?yàn)閙ock對(duì)象可以動(dòng)態(tài)創(chuàng)建任意屬性,因此適合替換任何對(duì)象。使用前面的示例,如果你mocking json庫(kù)并調(diào)用dumps(),則mock對(duì)象將創(chuàng)建該方法,以便其接口可以匹配庫(kù)原來(lái)的接口:

    >>> json = Mock() >>> json.dumps() <Mock name='mock.dumps()' id='4392249776'>

    ? 請(qǐng)注意, 這個(gè)mock對(duì)象的dumps()方法有兩個(gè)關(guān)鍵特點(diǎn):

  • 不同于原來(lái)的dumps()方法,這個(gè)模擬方法不需要參數(shù)(實(shí)際上,它會(huì)接受您傳遞給它的任何參數(shù))

  • dumps()方法的返回值也是一個(gè)Mock類(lèi)實(shí)例。Mock以遞歸方式定義其他Mock類(lèi)實(shí)例的特性允許你在復(fù)雜情況下游刃有余。

    >>> json = Mock() >>> json.loads('{"k": "v"}').get('k') <Mock name='mock.loads().get()' id='4379599424'>
  • 使用Mock object

    ? Mock類(lèi)實(shí)例存儲(chǔ)有關(guān)你如何使用它們的數(shù)據(jù)(*此處劃重點(diǎn)*),如,你可以看到一個(gè)方法是否被調(diào)用、調(diào)用了幾次、怎么調(diào)用的等等:

    >>> from unittest.mock import Mock>>> # Create a mock object ... json = Mock()>>> json.loads('{"key": "value"}') <Mock name='mock.loads()' id='4550144184'>>>> # You know that you called loads() so you can >>> # make assertions to test that expectation ... json.loads.assert_called() >>> json.loads.assert_called_once() >>> json.loads.assert_called_with('{"key": "value"}') >>> json.loads.assert_called_once_with('{"key": "value"}')>>> json.loads('{"key": "value"}') <Mock name='mock.loads()' id='4550144184'>>>> # If an assertion fails, the mock will raise an AssertionError ... json.loads.assert_called_once() Traceback (most recent call last):File "<stdin>", line 1, in <module>File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 795, in assert_called_onceraise AssertionError(msg) AssertionError: Expected 'loads' to have been called once. Called 2 times.>>> json.loads.assert_called_once_with('{"key": "value"}') Traceback (most recent call last):File "<stdin>", line 1, in <module>File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 824, in assert_called_once_withraise AssertionError(msg) AssertionError: Expected 'loads' to be called once. Called 2 times.>>> json.loads.assert_not_called() Traceback (most recent call last):File "<stdin>", line 1, in <module>File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 777, in assert_not_calledraise AssertionError(msg) AssertionError: Expected 'loads' to not have been called. Called 2 times.

    ? 在上例中,assert_called()確保你調(diào)用了該方法,如果沒(méi)有調(diào)用就會(huì)報(bào)錯(cuò);而assert_called_once()檢查你是否只調(diào)用了一次該方法。這兩個(gè)斷言函數(shù)都有相應(yīng)的變體來(lái)檢查傳遞給該方法的參數(shù):

    • .assert_called_with(*args, **kwargs)

    • .assert_called_once_with(*args, **kwargs)

      ? 要通過(guò)這些斷言測(cè)試,必須使用傳遞給實(shí)際方法的相同參數(shù)調(diào)用mocked方法(*此處劃重點(diǎn)*)

    >>> json = Mock() >>> json.loads(s='{"key": "value"}') >>> json.loads.assert_called_with('{"key": "value"}') Traceback (most recent call last):File "<stdin>", line 1, in <module>File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 814, in assert_called_withraise AssertionError(_error_message()) from cause AssertionError: Expected call: loads('{"key": "value"}') Actual call: loads(s='{"key": "value"}') >>> json.loads.assert_called_with(s='{"key": "value"}')

    ? 另外,你還可以查看mock對(duì)象的特殊屬性來(lái)測(cè)試你的應(yīng)用程序如何使用被替換對(duì)象:

    >>> from unittest.mock import Mock>>> # Create a mock object ... json = Mock() >>> json.loads('{"key": "value"}') <Mock name='mock.loads()' id='4391026640'>>>> # Number of times you called loads(): ... json.loads.call_count 1 >>> # The last loads() call: ... json.loads.call_args call('{"key": "value"}') >>> # List of loads() calls: ... json.loads.call_args_list [call('{"key": "value"}')] >>> # List of calls to json's methods (recursively): ... json.method_calls [call.loads('{"key": "value"}')]

    ? 你可以使用這些屬性來(lái)編寫(xiě)測(cè)試代碼,以確保程序是否按照你的預(yù)期來(lái)運(yùn)行。這里先總結(jié)一下Mock對(duì)象的各類(lèi)方法

  • 構(gòu)造方法

    __init__(name=None, return_value=DEFAULT, side_effect=None, spec=None),spec設(shè)置的是mock對(duì)象的屬性,可以是property或者方法,也可以是其他的列表字符串或者其他的python類(lèi)。

    spec can be either a list of strings or an existing object (a class or instance) that acts as the specification for the mock object. If you pass in an object then a list of strings is formed by calling dir on the object (excluding unsupported magic attributes and methods). Accessing any attribute not in this list will raise an AttributeError.

  • assert方法

    assert_called() # 斷言該mock對(duì)象被調(diào)用了 assert_called_once() # 斷言該mock對(duì)象被調(diào)用了一次 assert_called_once_with() # 斷言該mock對(duì)象以什么參數(shù)被調(diào)用了一次 assert_called_with() # 斷言該mock對(duì)象以什么參數(shù)被調(diào)用過(guò) assert_not_called() # 斷言該mock對(duì)象沒(méi)有調(diào)用過(guò) assert_any_call() # 用于檢查測(cè)試的mock對(duì)象在測(cè)試?yán)讨惺欠裾{(diào)用了方法 assert_has_calls() # 檢查是否按照正確的順序和正確的參數(shù)進(jìn)行調(diào)用的
  • 統(tǒng)計(jì)方法

    called() # 跟蹤mock對(duì)象所做的任意調(diào)用的訪(fǎng)問(wèn)器,返回bool值 call_count() # 調(diào)用次數(shù) call_args() # 最近一次調(diào)用參數(shù) call_args_list() # 所有調(diào)用的參數(shù)list mock_calls() # method_calls() # 試一個(gè)mock對(duì)象都調(diào)用了哪些方法,結(jié)果是一個(gè)list
  • 實(shí)用方法

    attach_mock() # 將一個(gè)mock對(duì)象添加到另一個(gè)mock對(duì)象中 configure_mock() # 配置Mock對(duì)象,包括name, side_effect,return_value等 mock_add_spec() # 給mock對(duì)象添加新的屬性 reset_mock() # 將mock對(duì)象恢復(fù)到測(cè)試之前的狀態(tài)
  • 屬性

    name # mock對(duì)象的唯一標(biāo)識(shí) return_value # 返回值 side_effect # 當(dāng)其不是DEFAULT時(shí),覆蓋return_value
  • 定制化Mock object

    return_value

    ? 使用mock模塊的重要原因之一就是為了能夠在測(cè)試期間控制代碼的行為,最簡(jiǎn)單的一種方式就是指定函數(shù)的返回值(return_value)。讓我們用一個(gè)例子來(lái)看看它是如何工作的。

    ? 首先,創(chuàng)建一個(gè)名為my_calendar.py的文件并編寫(xiě)一個(gè)is_weekday()函數(shù),用于判斷今天是否是工作日。最后,編寫(xiě)一個(gè)測(cè)試,確保函數(shù)的正確性:

    from datetime import datetimedef is_weekday():today = datetime.today()# Python's datetime library treats Monday as 0 and Sunday as 6return (0 <= today.weekday() < 5)# Test if today is a weekday assert is_weekday()

    ? 由于這里測(cè)試的是今天是否為工作日,因此結(jié)果取決于你進(jìn)行測(cè)試的那一天,這就意外著你今天測(cè)試成功了,說(shuō)不定過(guò)兩天到了周末,就測(cè)試失敗了。為了使測(cè)試結(jié)果的穩(wěn)定性,我們可以使用Mock來(lái)實(shí)現(xiàn)該測(cè)試:

    import datetime from unittest.mock import Mock# Save a couple of test days tuesday = datetime.datetime(year=2019, month=1, day=1) saturday = datetime.datetime(year=2019, month=1, day=5)# Mock datetime to control today's date datetime = Mock()def is_weekday():today = datetime.datetime.today()# Python's datetime library treats Monday as 0 and Sunday as 6return (0 <= today.weekday() < 5)# Mock .today() to return Tuesday datetime.datetime.today.return_value = tuesday # Test Tuesday is a weekday assert is_weekday() # Mock .today() to return Saturday datetime.datetime.today.return_value = saturday # Test Saturday is not a weekday assert not is_weekday()

    ? 在上面示例中,.today()是變成了一個(gè)模擬,并指定了它的return_value。這樣,當(dāng)你調(diào)用.today()時(shí),它會(huì)返回你指定的日期時(shí)間,實(shí)現(xiàn)測(cè)試的穩(wěn)定性。

    side_effect

    ? 在更復(fù)雜的場(chǎng)景中,也許僅僅控制return_value并不足以實(shí)現(xiàn)相關(guān)的業(yè)務(wù)邏輯。比如有時(shí)候,你期望當(dāng)一個(gè)測(cè)試函數(shù)被多次調(diào)用時(shí),你想讓函數(shù)返回不同的值甚至引發(fā)異常,則可以使用.side_effect來(lái)做到這一點(diǎn)。我們?cè)賹?xiě)一個(gè)函數(shù)來(lái)解釋這一特性:

    import requestsdef get_holidays():r = requests.get('http://localhost/api/holidays')if r.status_code == 200:return r.json()return None

    ? get_holidays()向localhost服務(wù)器發(fā)出請(qǐng)求,試圖獲得holiday信息。如果服務(wù)器響應(yīng)成功,get_holidays()將返回一個(gè)字典。否則,該方法將返回None。可以通過(guò)設(shè)置requests.get.side_effect來(lái)測(cè)試get_holidays()如何響應(yīng)連接超時(shí):

    import unittest from requests.exceptions import Timeout from unittest.mock import Mock# Mock requests to control its behavior requests = Mock()def get_holidays():r = requests.get('http://localhost/api/holidays')if r.status_code == 200:return r.json()return Noneclass TestCalendar(unittest.TestCase):def test_get_holidays_timeout(self):# Test a connection timeoutrequests.get.side_effect = Timeoutwith self.assertRaises(Timeout):get_holidays()if __name__ == '__main__':unittest.main()

    ? 如果你想要讓結(jié)果更加動(dòng)態(tài),可以將.side_effect設(shè)置為一個(gè)函數(shù),該函數(shù)與被mocking的函數(shù)共享參數(shù)(下面例子中,傳入request.get()的參數(shù),也會(huì)傳入log_request()):

    import requests import unittest from unittest.mock import Mock# Mock requests to control its behavior requests = Mock()def get_holidays():r = requests.get('http://localhost/api/holidays')if r.status_code == 200:return r.json()return Noneclass TestCalendar(unittest.TestCase):def log_request(self, url):# Log a fake request for test output purposesprint(f'Making a request to {url}.')print('Request received!')# Create a new Mock to imitate a Responseresponse_mock = Mock()response_mock.status_code = 200response_mock.json.return_value = {'12/25': 'Christmas','7/4': 'Independence Day',}return response_mockdef test_get_holidays_logging(self):# Test a successful, logged requestrequests.get.side_effect = self.log_requestassert get_holidays()['12/25'] == 'Christmas'if __name__ == '__main__':unittest.main()

    ? .side_effect也可以賦值為一個(gè)可迭代對(duì)象,其中必須包含返回值,異常或兩者的皆有。每次調(diào)用mocked方法時(shí),iterable都會(huì)返回下一個(gè)值。例如,您可以在Timeout返回成功響應(yīng)后測(cè)試重試:

    mport unittest from requests.exceptions import Timeout from unittest.mock import Mock# Mock requests to control its behavior requests = Mock()def get_holidays():r = requests.get('http://localhost/api/holidays')if r.status_code == 200:return r.json()return Noneclass TestCalendar(unittest.TestCase):def test_get_holidays_retry(self):# Create a new Mock to imitate a Responseresponse_mock = Mock()response_mock.status_code = 200response_mock.json.return_value = {'12/25': 'Christmas','7/4': 'Independence Day',}# Set the side effect of .get()requests.get.side_effect = [Timeout, response_mock]# Test that the first request raises a Timeoutwith self.assertRaises(Timeout):get_holidays()# Now retry, expecting a successful responseassert get_holidays()['12/25'] == 'Christmas'# Finally, assert .get() was called twiceassert requests.get.call_count == 2if __name__ == '__main__':unittest.main()

    ? 這里劃重點(diǎn),總結(jié)一下,side_effect可以是一個(gè)函數(shù)、一個(gè)Exception類(lèi)、或是一個(gè)包含二者的可迭代對(duì)象。當(dāng)作為函數(shù)時(shí),該函數(shù)將會(huì)傳入和mocked方法一樣的參數(shù);當(dāng)作為Exception,則不會(huì)返回,而是直接拋出異常。

    配置Mock

    ? 設(shè)置Mock上.return_value和.side_effect可以使用上面例子所采用的方法。但是,那并不是最靈活的方式,所以,本節(jié)主要探討一下Mock各種屬性的設(shè)置方式。

    ? 首先,你可以在初始化Mock實(shí)例時(shí)通過(guò)指定某些屬性來(lái)配置Mock:

    >>> mock = Mock(side_effect=Exception) >>> mock() Traceback (most recent call last):File "<stdin>", line 1, in <module>File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 939, in __call__return _mock_self._mock_call(*args, **kwargs)File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 995, in _mock_callraise effect Exception>>> mock = Mock(name='Real Python Mock') >>> mock <Mock name='Real Python Mock' id='4434041432'>>>> mock = Mock(return_value=True) >>> mock() True

    ? 雖然可以在Mock實(shí)例上設(shè)置.side_effect和.return_value,但是.name等其他屬性只能通過(guò).__ init __()或.configure_mock()設(shè)置。如果你嘗試在實(shí)例上設(shè)置Mock實(shí)例的.name屬性,你將會(huì)得到意想不到的結(jié)果:

    >>> mock = Mock(name='Real Python Mock') >>> mock.name <Mock name='Real Python Mock.name' id='4434041544'>>>> mock = Mock() >>> mock.name = 'Real Python Mock' >>> mock.name 'Real Python Mock'

    ? 你可以使用.configure_mock()配置現(xiàn)有的Mock實(shí)例:

    >>> mock = Mock() >>> mock.configure_mock(return_value=True) >>> mock() True

    ? 可以將一個(gè)字典數(shù)據(jù)傳到.configure_mock()或Mock .__ init __(),實(shí)現(xiàn)對(duì)Mock實(shí)例的屬性配置:

    # Verbose, old Mock response_mock = Mock() response_mock.json.return_value = {'12/25': 'Christmas','7/4': 'Independence Day', }# Shiny, new .configure_mock() holidays = {'12/25': 'Christmas', '7/4': 'Independence Day'} response_mock = Mock(**{'json.return_value': holidays})

    ?

    patch

    ? 在了解了mock對(duì)象之后,我們來(lái)看兩個(gè)方便測(cè)試的函數(shù):patch和patch.object。這兩個(gè)函數(shù)都會(huì)返回一個(gè)mock內(nèi)部的類(lèi)實(shí)例,這個(gè)類(lèi)是class _patch。返回的這個(gè)類(lèi)實(shí)例既可以作為函數(shù)的裝飾器,也可以作為類(lèi)的裝飾器,也可以作為上下文管理器。使用patch或者patch.object的目的是為了控制mock的范圍,意思就是在一個(gè)函數(shù)范圍內(nèi),或者一個(gè)類(lèi)的范圍內(nèi),或者with語(yǔ)句的范圍內(nèi)mock掉一個(gè)對(duì)象

    as a Decorator

    ? 如果要在整個(gè)測(cè)試函數(shù)中mocking某個(gè)對(duì)象,可以使用patch()作為函數(shù)裝飾器。為了探究它的工作原理,將邏輯代碼和測(cè)試代碼放入單獨(dú)的文件來(lái)重新組織my_calendar.py文件:

    import requests from datetime import datetimedef is_weekday():today = datetime.today()# Python's datetime library treats Monday as 0 and Sunday as 6return 0 <= today.weekday() < 5def get_holidays():r = requests.get('http://localhost/api/holidays')if r.status_code == 200:return r.json()return None

    ? 這些函數(shù)現(xiàn)在位于單獨(dú)的文件中,與測(cè)試代碼完全分離。接下來(lái),在test.py文件中編寫(xiě)測(cè)試代碼:

    import unittest from my_calendar import get_holidays from requests.exceptions import Timeout from unittest.mock import patchclass TestCalendar(unittest.TestCase):@patch('my_calendar.requests')def test_get_holidays_timeout(self, mock):mock.get.side_effect = Timeoutwith self.assertRaises(Timeout):get_holidays()mock.get.assert_called_once()if __name__ == '__main__':unittest.main()

    ? 上面測(cè)試代碼中,首先在測(cè)試函數(shù)范圍內(nèi)創(chuàng)建了Mock類(lèi)實(shí)例 mock,該mock對(duì)象在測(cè)試函數(shù)范圍內(nèi)替換了my_calendar.py中的requests。

    Technical Detail: patch() returns an instance of MagicMock, which is a Mocksubclass. MagicMock is useful because it implements most magic methods for you, such as .__len__(), .__str__(), and .__iter__(), with reasonable defaults.

    as a Context Manager

    ? 有時(shí)候,你會(huì)想將patch()作為上下文管理器來(lái)使用,比如:

    • 你只想模擬替換測(cè)試范圍的一部分對(duì)象;

    • 您已經(jīng)使用了太多的裝飾器或參數(shù),這會(huì)降低測(cè)試代碼的可讀性;

      舉個(gè)例子(更多例子可以參考cookbook):

    import unittest from my_calendar import get_holidays from requests.exceptions import Timeout from unittest.mock import patchclass TestCalendar(unittest.TestCase):def test_get_holidays_timeout(self):with patch('my_calendar.requests') as mock_requests:mock_requests.get.side_effect = Timeoutwith self.assertRaises(Timeout):get_holidays()mock_requests.get.assert_called_once()if __name__ == '__main__':unittest.main()

    patch.object

    ? 到目前為止,我們替換的是整個(gè)完整的對(duì)象,但有時(shí)也許只想模擬替換一個(gè)對(duì)象的一部分。這時(shí)候可以使用path.object()來(lái)實(shí)現(xiàn):

    import unittest from my_calendar import requests, get_holidays from unittest.mock import patchclass TestCalendar(unittest.TestCase):@patch.object(target=requests, attribute='get', side_effect=requests.exceptions.Timeout)def test_get_holidays_timeout(self, mock_requests):with self.assertRaises(requests.exceptions.Timeout):get_holidays()if __name__ == '__main__':unittest.main()

    除了對(duì)象和屬性,你還可以使用patch.dict()模擬替換dict

    寫(xiě)在篇后

    ??本文主要介紹了unittest的基本用法,包括基本特性、命令行操作、測(cè)試流程組織、Mock模塊的靈活運(yùn)用等。俗話(huà)說(shuō),寫(xiě)不寫(xiě)測(cè)試的代碼,就是耍流氓,希望看了這篇之后,你可以不做流氓!

    總結(jié)

    以上是生活随笔為你收集整理的万字长文深度解析python 单元测试的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。