例)
前言u(píng)nittest是 Python 標(biāo)準(zhǔn)庫自帶的測試框架不需要安裝任何第三方包。它的設(shè)計(jì)借鑒了 Java 的 JUnit所以你會(huì)看到TestCase、setUp、tearDown這類命名。雖然現(xiàn)在很多人轉(zhuǎn)向 pytest但unittest依然值得學(xué)一是零依賴二是大量存量項(xiàng)目用它三是理解它的結(jié)構(gòu)后看別的測試框架會(huì)輕松很多。這篇是實(shí)操向從寫第一個(gè)TestCase開始講到assertEqual系列斷言、setUp/tearDown的生命周期、assertRaises怎么驗(yàn)證異常、unittest.mock怎么打樁最后說清測試目錄該怎么組織、怎么用一條命令跑起來。文中所有 API 名稱與參數(shù)以 Python 官方文檔unittest與unittest.mock頁面為準(zhǔn)。示例以 Python 3 為基準(zhǔn)。一、第一個(gè) TestCase核心結(jié)構(gòu)是繼承unittest.TestCase把每個(gè)測試寫成一個(gè)以test_開頭的方法。# 適用于 Python 3.8import unittestdef add(a, b):return a bdef divide(a, b):if b 0:raise ValueError(除數(shù)不能為 0)return a / bclass TestCalculator(unittest.TestCase):def test_add(self):self.assertEqual(add(2, 3), 5)def test_divide(self):self.assertEqual(divide(10, 2), 5)def test_divide_by_zero(self):with self.assertRaises(ValueError):divide(1, 0)if __name__ __main__:unittest.main()幾點(diǎn)解釋方法名必須以test開頭否則測試加載器不會(huì)收集它。這是最常見的「寫了測試卻一個(gè)都沒跑」的原因。unittest.main()會(huì)自動(dòng)發(fā)現(xiàn)本模塊里的測試并運(yùn)行方便單文件直接python xxx.py執(zhí)行。斷言失敗時(shí)unittest會(huì)打印期望值與實(shí)際值并指出是哪個(gè)方法、哪一行。二、常用斷言方法TestCase提供了一整套以assert開頭的方法。不要用裸assert語句因?yàn)閍ssert在-O優(yōu)化模式下會(huì)被整體移除測試就形同虛設(shè)。用斷言方法還能得到更好的失敗信息。方法用途備注assertEqual(a, b)判斷相等最常用底層用assertNotEqual(a, b)判斷不等assertTrue(x)/assertFalse(x)判斷真假assertIs(a, b)/assertIsNot(a, b)判斷同一對(duì)象比assertEqual嚴(yán)格assertIsNone(x)判斷是NoneassertIn(a, b)/assertNotIn(a, b)成員判斷assertRaises(exc)斷言拋異常上下文管理器用法assertAlmostEqual(a, b)浮點(diǎn)近似相等避免浮點(diǎn)直接比較assertCountEqual(a, b)兩序列元素相同不計(jì)順序一個(gè)補(bǔ)充說明舊的assertEquals、assertNotEquals是assertEqual、assertNotEqual的別名官方已把它們標(biāo)記為廢棄deprecated新代碼應(yīng)當(dāng)用不帶s的版本。浮點(diǎn)數(shù)比較要特別注意0.1 0.2 0.3在二進(jìn)制浮點(diǎn)下不成立所以應(yīng)該用assertAlmostEqual# 適用于 Python 3.8import unittestclass TestFloat(unittest.TestCase):def test_float_sum(self):self.assertAlmostEqual(0.1 0.2, 0.3, places7)def test_raises_with_message(self):with self.assertRaises(KeyError) as ctx:{}[missing]self.assertIn(missing, str(ctx.exception))三、setUp 與 tearDown測試夾具測試之間應(yīng)當(dāng)相互獨(dú)立一個(gè)測試的狀態(tài)不能泄漏到下一個(gè)。setUp()在每個(gè)測試方法之前運(yùn)行tearDown()在每個(gè)測試方法之后運(yùn)行——用它們準(zhǔn)備和清理夾具fixture。# 適用于 Python 3.8import unittestclass Calculator:def __init__(self):self.history []def add(self, a, b):result a bself.history.append(result)return resultclass TestCalculator(unittest.TestCase):def setUp(self):# 每個(gè)測試都拿到一個(gè)全新的實(shí)例互不污染self.calc Calculator()def tearDown(self):# 每個(gè)測試結(jié)束后清理這里沒有外部資源僅演示位置self.calc Nonedef test_add_records_history(self):self.calc.add(1, 2)self.assertEqual(self.calc.history, [3])def test_history_starts_empty(self):# 上一個(gè)測試加過的結(jié)果不會(huì)出現(xiàn)在這里self.assertEqual(self.calc.history, [])生命周期規(guī)則要記準(zhǔn)setUp()拋異常該測試方法不會(huì)執(zhí)行tearDown()也不會(huì)執(zhí)行如果setUp()成功則無論測試方法是否失敗tearDown()都會(huì)執(zhí)行更粗粒度還有setUpClass()/tearDownClass()用classmethod裝飾整個(gè)類跑一次和setUpModule()/tearDownModule()整個(gè)模塊跑一次適合建立昂貴的、所有測試共享的資源比如臨時(shí)數(shù)據(jù)庫。四、用 mock 隔離外部依賴單元測試只應(yīng)測「被測單元」不該真的去連數(shù)據(jù)庫、發(fā) HTTP 請(qǐng)求。unittest.mock用來把這些外部依賴替換成可控的假對(duì)象。最常用的是patch它既可以當(dāng)裝飾器也可以當(dāng)上下文管理器。# 適用于 Python 3.8# 被測模塊 weather.py 里寫# import requests# def get_temp(city):# resp requests.get(fhttps://api.example.com/{city})# return resp.json()[temp]import unittestfrom unittest.mock import patch, MagicMockimport weather # 上面那個(gè)被測模塊class TestGetTemp(unittest.TestCase):patch(weather.requests) # patch 被測模塊里的名字不是全局 requestsdef test_get_temp(self, mock_requests):mock_requests.get.return_value.json.return_value {temp: 21}self.assertEqual(weather.get_temp(shanghai), 21)mock_requests.get.assert_called_once_with(https://api.example.com/shanghai)def test_with_magicmock_argument(self):# 另一種做法把假的 client 當(dāng)參數(shù)注入不碰請(qǐng)求庫client MagicMock()client.get.return_value.json.return_value {name: Ada}resp client.get(/users/1)self.assertEqual(resp.json()[name], Ada)關(guān)鍵要點(diǎn)patch 的位置要 patch「使用它的地方」而不是「定義它的地方」。上面被測模塊weather.py里import requests后調(diào)用requests.get就應(yīng)該 patchweather.requests這個(gè)名字而不是全局的requests。MagicMock會(huì)自動(dòng)生成屬性和方法所以client.get().json()這種鏈?zhǔn)秸{(diào)用不需要手工打樁。assert_called_once_with(...)、assert_called_with(...)用來驗(yàn)證調(diào)用參數(shù)是「行為測試」的常用手段。想模擬異常給 mock 設(shè)side_effect。五、測試目錄怎么組織怎么跑起來推薦把源碼和測試分開放測試文件名以test_開頭這樣unittest的自動(dòng)發(fā)現(xiàn)能識(shí)別project/├── myservice/│ ├── __init__.py│ └── calculator.py└── tests/├── __init__.py├── test_calculator.py└── test_api.py運(yùn)行方式有三種# 1. 跑單個(gè)文件python -m unittest tests.test_calculator# 2. 自動(dòng)發(fā)現(xiàn)當(dāng)前目錄下所有 test*.pypython -m unittest discover# 3. 指定起始目錄與匹配模式python -m unittest discover -s tests -p test_*.py -v注意discover默認(rèn)匹配的是test*.pytest開頭不是*_test.py。如果你的文件命名成calculator_test.py默認(rèn)發(fā)現(xiàn)不了——要么改文件名要么用-p指定模式。另外測試目錄里放一個(gè)__init__.py能讓「按包名運(yùn)行」在多數(shù)配置下更穩(wěn)妥。常見坑點(diǎn)測試方法不以test開頭?def check_add(self): ...?def test_add(self): ...用裸 assert 而不是斷言方法?assert add(1, 1) 2-O下會(huì)被優(yōu)化掉?self.assertEqual(add(1, 1), 2)測試之間共享可變狀態(tài)? 在類屬性里建一個(gè)列表多個(gè)測試往里塞數(shù)據(jù)? 在setUp()里每個(gè)測試新建獨(dú)立實(shí)例浮點(diǎn)數(shù)直接assertEqual?self.assertEqual(0.1 0.2, 0.3)不相等?self.assertAlmostEqual(0.1 0.2, 0.3)patch 錯(cuò)了目標(biāo)? patch 庫定義處結(jié)果被測模塊用了自己的引用沒被替換? patch 被測模塊里「引用它的那個(gè)名字」測試?yán)镒稣鎸?shí)的網(wǎng)絡(luò) / 數(shù)據(jù)庫訪問? 測試依賴外部服務(wù)是否在線慢且不穩(wěn)? 用mock隔離外部依賴文件名不匹配發(fā)現(xiàn)規(guī)則?calculator_test.py用默認(rèn)discover跑不到? 改成test_calculator.py或用-p指定模式依賴tearDown在setUp失敗時(shí)執(zhí)行?setUp拋異常還想靠tearDown清理? 記住setUp失敗時(shí)tearDown不會(huì)執(zhí)行清理邏輯要能容忍總結(jié)概念作用關(guān)鍵約束TestCase測試容器方法名以test開頭setUp/tearDown每個(gè)測試前后setUp失敗則tearDown不跑setUpClass/tearDownClass每類一次需classmethod斷言方法驗(yàn)證結(jié)果別用裸assertmock.patch隔離依賴patch 使用處而非定義處discover批量運(yùn)行默認(rèn)匹配test*.pyunittest的骨架說穿了就四塊繼承TestCase、test_命名、setUp準(zhǔn)備、斷言驗(yàn)證。把這四塊用熟練再學(xué) pytest 只是換個(gè)更順手的皮。