万字长文深度解析python 单元测试
文章目錄
- 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)始搜尋的目錄
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ù)量。
-
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 部分:
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)*):
? 另外,你還可以查看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):
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)題。
- 上一篇: python Logging日志记录模块
- 下一篇: python scipy 稀疏矩阵详解