化測(cè)試框架遷移實(shí)戰(zhàn)與最佳實(shí)踐)
寫測(cè)試代碼這件事我折騰了快十年從最早手寫腳本到unittest再到Pytest最大的感受就一句話測(cè)試代碼寫得好不好不取決于你寫了多少用例而取決于整套測(cè)試跑起來(lái)是否“舒坦”。Pytest就是那種能讓你從“應(yīng)付差事”變成“愿意維護(hù)”的框架。這篇東西不是官方文檔的復(fù)述是我自己從unittest痛苦遷移過(guò)來(lái)的實(shí)操記錄覆蓋安裝、工程結(jié)構(gòu)、fixture、參數(shù)化、報(bào)告和排坑適合剛接觸自動(dòng)化測(cè)試的新手也適合已經(jīng)在用unittest但想換一套更順手工具的人。很多朋友第一次接觸Pytest都是從“pytest比unittest好用”這句話開(kāi)始的但到底好在哪里、怎么用才算用得優(yōu)雅很少有文章講透。這篇我盡量用實(shí)際場(chǎng)景說(shuō)話把踩過(guò)的坑和驗(yàn)證過(guò)的寫法都攤開(kāi)來(lái)聊。1. 為什么是Pytest先從unittest的痛點(diǎn)說(shuō)起1.1 unittest讓人最難受的幾個(gè)地方如果你是從unittest起步的下面這些場(chǎng)景你應(yīng)該不陌生。首先是那套固定的類繼承測(cè)試用例必須寫在unittest.TestCase的子類里方法名必須以test開(kāi)頭想打破這個(gè)套路就得翻官方文檔去折騰。類本身沒(méi)問(wèn)題問(wèn)題在于它把所有東西都綁死在一套嚴(yán)格的OOP范式里寫簡(jiǎn)單用例時(shí)感覺(jué)冗余寫復(fù)雜用例時(shí)又覺(jué)得不夠靈活。更難受的是前置條件和清理邏輯。假設(shè)我有一批需要登錄態(tài)的接口測(cè)試用unittest寫我得在setUp里做登錄、初始化數(shù)據(jù)、拼接請(qǐng)求頭然后在每個(gè)用例里重復(fù)調(diào)用到了tearDown還得記得清理測(cè)試數(shù)據(jù)。用例少的時(shí)候還好一旦用例超過(guò)幾十條setUp和tearDown就開(kāi)始各種套娃一個(gè)類里塞滿了和業(yè)務(wù)驗(yàn)證無(wú)關(guān)的準(zhǔn)備工作。最經(jīng)典的痛點(diǎn)是一個(gè)用例可能需要多個(gè)不同的前置環(huán)境但unittest的setUp只能寫一套想根據(jù)不同場(chǎng)景切換就得把類拆得稀碎。還有斷言。unittest自帶一整套assertEqual、assertIn、assertTrue這種API問(wèn)題不在功能而在失敗時(shí)的可讀性。我見(jiàn)過(guò)太多同事盯著AssertionError: False is not true發(fā)呆根本不知道到底是哪一步出了問(wèn)題只能靠print大法一條條去追。這個(gè)體驗(yàn)怎么說(shuō)呢就好比你跟朋友約好了見(jiàn)面地點(diǎn)對(duì)方只回復(fù)你一句“不對(duì)”你根本不知道是地址錯(cuò)了還是時(shí)間錯(cuò)了。1.2 Pytest的核心設(shè)計(jì)思路Pytest解決這些問(wèn)題的思路很直接別搞那么多條條框框讓Python自己的語(yǔ)法來(lái)干活。測(cè)試函數(shù)就是普通函數(shù)只要文件名以test_開(kāi)頭或_test結(jié)尾、函數(shù)名以test開(kāi)頭運(yùn)行pytest命令時(shí)就會(huì)被自動(dòng)發(fā)現(xiàn)。不再需要繼承任何基類不用記住幾十個(gè)斷言API的名字。斷言這塊更是把“少即是多”做到了極致。Pytest直接復(fù)用Python原生的assert語(yǔ)句比如assert user[name] 張三斷言失敗時(shí)它會(huì)自動(dòng)打印出表達(dá)式兩邊實(shí)際的值。我不用再猜一眼就能看到左邊是李四右邊是張三差異在哪清清楚楚。就這個(gè)能力遷移之后我整個(gè)排查測(cè)試失敗的時(shí)間少了一半不止。fixture機(jī)制算是Pytest最核心的設(shè)計(jì)了。簡(jiǎn)單理解fixture就是一個(gè)帶裝飾器的函數(shù)負(fù)責(zé)準(zhǔn)備數(shù)據(jù)和環(huán)境測(cè)試函數(shù)用參數(shù)名直接聲明需要哪些fixturePytest自動(dòng)把返回值注入進(jìn)去。它把unittest的setUp/tearDown拆成了更靈活、更細(xì)粒度的模塊可以單獨(dú)定義登錄態(tài)、單獨(dú)定義數(shù)據(jù)庫(kù)數(shù)據(jù)、單獨(dú)定義臨時(shí)文件然后隨意組合。1.3 什么場(chǎng)景下我仍然不推薦Pytest不是所有項(xiàng)目都適合無(wú)腦上Pytest。有兩個(gè)反例我實(shí)際遇到過(guò)。一個(gè)是極簡(jiǎn)場(chǎng)景總共就二三十個(gè)用例純本地腳本、不接CI、幾乎沒(méi)有復(fù)用需求那用unittest也一樣能跑多裝一個(gè)依賴反而增加環(huán)境負(fù)擔(dān)。另一個(gè)是對(duì)測(cè)試代碼有極端定制需求的場(chǎng)景比如你要強(qiáng)制所有用例都遵循完全統(tǒng)一的OOP繼承鏈團(tuán)隊(duì)又對(duì)夾具注入這套機(jī)制非常不熟悉那遷移的陣痛期會(huì)比想象中長(zhǎng)。還有一個(gè)情況要特別提醒Pytest雖然對(duì)新手友好但它內(nèi)部其實(shí)很靈活同一個(gè)需求有七八種寫法。靈活意味著團(tuán)隊(duì)容易寫出風(fēng)格迥異的代碼反而需要你提前定好規(guī)范。我見(jiàn)過(guò)一個(gè)項(xiàng)目fixture有四種定義方式、兩種命名風(fēng)格跑到最后測(cè)試代碼的可讀性比被測(cè)代碼還差。工具解決不了人的問(wèn)題這點(diǎn)后面會(huì)細(xì)說(shuō)。2. 環(huán)境準(zhǔn)備安裝、工程目錄與PyCharm配置2.1 用pip安裝pytest初學(xué)者最容易卡住的反而是環(huán)境的整潔度。我強(qiáng)烈建議你在虛擬環(huán)境里操作別圖省事直接裝到系統(tǒng)Python里。第一步創(chuàng)建虛擬環(huán)境python -m venv venv然后激活它Windows下執(zhí)行venv\Scripts\activatemacOS/Linux下執(zhí)行source venv/bin/activate。接著安裝pip install pytest裝完驗(yàn)證一下pytest --version能輸出版本號(hào)就說(shuō)明成了。實(shí)際項(xiàng)目里我更建議直接用一個(gè)requirements.txt把測(cè)試相關(guān)依賴鎖住后續(xù)同事拉代碼只需要一條命令就能復(fù)現(xiàn)環(huán)境pip install -r requirements.txt文件內(nèi)容可以寫成這樣pytest7.4.3 pytest-html4.1.0 pytest-xdist3.3.1 pytest-cov4.1.0 requests2.31.0鎖版本號(hào)這件事很多人嫌麻煩但測(cè)試環(huán)境的穩(wěn)定性恰恰就靠這個(gè)。你永遠(yuǎn)不知道同事機(jī)器上那個(gè)“稍新一點(diǎn)的pytest”會(huì)不會(huì)因?yàn)槟硞€(gè)行為變化就讓整個(gè)套件紅掉。2.2 標(biāo)準(zhǔn)的工程目錄長(zhǎng)什么樣Pytest對(duì)目錄結(jié)構(gòu)沒(méi)有硬性要求但用多了你會(huì)發(fā)現(xiàn)一套好用的默認(rèn)約定。我目前比較推薦的工程結(jié)構(gòu)是這樣的project/ ├── src/ │ ├── __init__.py │ ├── api_client.py │ └── models.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_user_api.py │ ├── test_order_flow.py │ └── test_cart.py ├── requirements.txt └── pytest.ini關(guān)鍵點(diǎn)在tests目錄下的conftest.py。這個(gè)文件是Pytest的“全局配置中樞”里面定義的fixture可以被tests下所有測(cè)試文件直接使用不需要任何import。這一點(diǎn)解決了很多人在unittest里糾纏不清的公共依賴問(wèn)題——登錄狀態(tài)、數(shù)據(jù)庫(kù)連接、臨時(shí)目錄統(tǒng)一寫在conftest.py里所有測(cè)試文件按需聲明即可。pytest.ini是配置文件我通常這樣寫[pytest] testpaths tests python_files test_*.py python_classes Test* python_functions test_* addopts -v -stestpaths指定了pytest運(yùn)行時(shí)要搜索的測(cè)試目錄避免在無(wú)關(guān)代碼里浪費(fèi)時(shí)間。addopts里的-v是詳細(xì)輸出-s是讓print內(nèi)容直接顯示。調(diào)試階段這倆我必開(kāi)不然print的內(nèi)容會(huì)被Pytest的捕獲機(jī)制吞掉很容易產(chǎn)生“代碼明明執(zhí)行了但啥都沒(méi)打印”的錯(cuò)覺(jué)。2.3 PyCharm里怎么配置用PyCharm的話有幾點(diǎn)值得先設(shè)置好。打開(kāi)Settings找到Project Interpreter確認(rèn)解釋器指向的是剛才那個(gè)虛擬環(huán)境里的Python而不是系統(tǒng)自帶的。然后在Tools下面有個(gè)Python Integrated Tools把Default test runner設(shè)為pytest。這兩步做完你在測(cè)試文件里點(diǎn)右鍵就能直接選Run pytest跑完之后點(diǎn)左側(cè)紅黃綠色的小條就能看到失敗詳情。PyCharm對(duì)Pytest的fixture跳轉(zhuǎn)也支持得很好按住Ctrl點(diǎn)函數(shù)參數(shù)里的fixture名能直接跳到定義位置。這點(diǎn)對(duì)讀別人寫的測(cè)試代碼特別有用不然光靠人肉找fixture定義會(huì)瘋掉。項(xiàng)目跑起來(lái)之后那個(gè)Run窗口里顯示的日志如果太亂可以在pytest.ini里調(diào)整日志級(jí)別加上log_cli true和log_cli_level INFO讓關(guān)鍵信息透出來(lái)。3. 核心特性實(shí)操斷言、fixture與參數(shù)化3.1 斷言就應(yīng)該用原生assertPytest把斷言簡(jiǎn)化到接近“說(shuō)人話”。看兩個(gè)例子就明白了def test_user_profile(): user get_user_by_id(1001) assert user is not None assert user[nickname] 老王 assert len(user[tags]) 3失敗了Pytest直接告訴你assert user[nickname] 老王 E AssertionError: assert 老李 老王你不需要任何額外解析錯(cuò)誤信息自帶現(xiàn)場(chǎng)。更絕的是對(duì)集合和字典的斷言def test_permission(): perms get_user_permissions(1001) assert {read, write} set(perms)如果失敗它能直接列出你期望的集合里哪些元素不在實(shí)際結(jié)果里。這種差距用過(guò)就回不去了。斷言異常的手段也要會(huì)用比如驗(yàn)證一個(gè)接口在參數(shù)錯(cuò)誤時(shí)拋出ValueErrorimport pytest def test_invalid_param_raises(): with pytest.raises(ValueError, matchid不能為空): create_order(user_idNone)match參數(shù)不是必須的但我建議能寫就寫它能把“確實(shí)拋了異常但拋的卻是另一個(gè)異?!钡那闆r暴露出來(lái)。3.2 fixture測(cè)試前置與清理的正確姿勢(shì)fixture的核心在“聲明式”這三個(gè)字上。比如我要準(zhǔn)備一個(gè)帶登錄態(tài)的API客戶端最基礎(chǔ)的寫法是這樣import pytest import requests pytest.fixture def auth_client(): token login(test_user, password) client requests.Session() client.headers.update({Authorization: fBearer {token}}) return client def test_get_orders(auth_client): resp auth_client.get(/api/orders) assert resp.status_code 200測(cè)試函數(shù)加一個(gè)參數(shù)auth_clientPytest就自動(dòng)幫你調(diào)好。如果同時(shí)需要多個(gè)前置直接加多個(gè)參數(shù)就行def test_create_order(auth_client, test_db): # auth_client負(fù)責(zé)登錄test_db負(fù)責(zé)準(zhǔn)備數(shù)據(jù)庫(kù)記錄 ...test_db是另一個(gè)fixture負(fù)責(zé)建表、插入測(cè)試數(shù)據(jù)用例跑完還能自動(dòng)清理。模塊在這里被拆得很干凈互相之間不耦合。fixture的清理邏輯放在yield后面pytest.fixture def temp_file(tmp_path): path tmp_path / data.txt path.write_text(hello, encodingutf-8) yield path # 到這里執(zhí)行清理邏輯 tmp_path.unlink(missing_okTrue)這個(gè)yield把“準(zhǔn)備”和“收尾”清晰地分隔開(kāi)了比unittest的tearDown更直觀。你在use fixture之后寫清理代碼它照樣會(huì)在用例結(jié)束時(shí)執(zhí)行哪怕用例中途拋異常也不會(huì)跳過(guò)。fixture還能控制作用域默認(rèn)是function也就是每個(gè)用例跑一遍。但有些資源完全不需要來(lái)回重復(fù)創(chuàng)建比如數(shù)據(jù)庫(kù)連接池、配置文件對(duì)象用pytest.fixture(scopemodule)讓整個(gè)模塊只創(chuàng)建一次速度能差出好幾倍。涉及到“模塊級(jí)只創(chuàng)建一次”的東西我建議順手把權(quán)限檢查也寫在fixture里避免被誤用。不過(guò)fixture有個(gè)讓很多新手困惑的地方默認(rèn)情況下一個(gè)測(cè)試函數(shù)如果聲明了fixture參數(shù)就必須依次執(zhí)行完fixture才能進(jìn)入測(cè)試體。如果你確實(shí)想讓某個(gè)fixture自動(dòng)生效、又不想在函數(shù)簽名里聲明可以用autouseTruepytest.fixture(autouseTrue) def enable_logging(): logging.basicConfig(levellogging.INFO)這種情況適合全局都要生效的橫切邏輯比如測(cè)試數(shù)據(jù)庫(kù)事務(wù)回滾。但autouse別濫用否則所有用例都被強(qiáng)制綁定某些依賴別人看代碼時(shí)很難一眼看出來(lái)為什么這個(gè)fixture生效了。3.3 參數(shù)化一份用例跑遍所有場(chǎng)景參數(shù)化是Pytest里最能提升“含金量”的特性。過(guò)去用unittest如果想對(duì)同一個(gè)接口用十組不同參數(shù)做驗(yàn)證要么寫十個(gè)方法要么用循環(huán)包一層。但循環(huán)包一層的后果就是如果第三組數(shù)據(jù)失敗了你只能看到“第3次迭代失敗”具體是哪組參數(shù)、為什么失敗還得自己拼回去。Pytest的pytest.mark.parametrize把數(shù)據(jù)和用例徹底分開(kāi)import pytest pytest.mark.parametrize( price, discount, expected, [ (100, 0.9, 90), (200, 0.5, 100), (50, 0.2, 10), (0, 0.1, 0), ], ) def test_calculate_discount(price, discount, expected): assert price * discount expected運(yùn)行之后Pytest會(huì)把每個(gè)參數(shù)組合當(dāng)成一條獨(dú)立用例輸出類似test_calculate_discount[100-0.9-90]這樣的名字。哪條掛了、掛在哪組數(shù)據(jù)上一眼就能定位。參數(shù)化還有兩個(gè)進(jìn)階技巧。一個(gè)是ids參數(shù)給每組數(shù)據(jù)起別名pytest.mark.parametrize( price, discount, expected, [ (100, 0.9, 90), (200, 0.5, 100), ], ids[九折, 半價(jià)], ) def test_calculate_discount(price, discount, expected): ...這樣生成報(bào)告的時(shí)候用例名不再是那串?dāng)?shù)字而是“九折”“半價(jià)”可讀性直接拉滿。另一個(gè)技巧是參數(shù)化里放fixture的返回值但那是比較高級(jí)的玩法先把基礎(chǔ)搞清楚再說(shuō)。3.4 標(biāo)記和條件跳過(guò)實(shí)際項(xiàng)目里總有幾種用例不能真跑依賴第三方環(huán)境但今天環(huán)境掛了、功能已知有bug且開(kāi)發(fā)還沒(méi)修、在本地調(diào)試時(shí)不想跑慢速用例。Pytest用marker處理這些場(chǎng)景最簡(jiǎn)單的是無(wú)條件跳過(guò)pytest.mark.skip(reason上游API暫未上線) def test_payment_notify(): ...如果想“跑一下看看知道失敗但我不讓它阻塞整個(gè)套件”用xfailpytest.mark.xfail(reason已知缺陷等待修復(fù)) def test_legacy_parsing(): ...xfail的妙處是如果某天這個(gè)用例突然過(guò)了Pytest會(huì)報(bào)告成XPASS這就是一個(gè)明確的信號(hào)開(kāi)發(fā)把bug修了這時(shí)候你可以把標(biāo)記撤掉了。這個(gè)機(jī)制能幫你維護(hù)一個(gè)“已知問(wèn)題清單”比在Excel里記錄靠譜得多。自定義marker也很實(shí)用。比如給接口測(cè)試、UI測(cè)試、冒煙測(cè)試打上不同標(biāo)簽然后在pytest.ini里統(tǒng)一注冊(cè)再配合-m smoke來(lái)單獨(dú)執(zhí)行冒煙用例集業(yè)務(wù)上靈活很多。4. 插件生態(tài)讓測(cè)試報(bào)告和效率起飛4.1 pytest-html零成本獲得一份網(wǎng)頁(yè)報(bào)告跑完測(cè)試之后黑壓壓的終端輸出不是不能看但要是想發(fā)給團(tuán)隊(duì)其他人或者沉淀歷史記錄一份HTML報(bào)告會(huì)體面很多。pytest-html用起來(lái)幾乎不需要學(xué)習(xí)成本pip install pytest-html pytest --htmlreport.html --self-contained-html--self-contained-html參數(shù)的意思是把樣式和腳本全部?jī)?nèi)嵌進(jìn)一個(gè)文件方便單文件轉(zhuǎn)發(fā)對(duì)方不需要聯(lián)網(wǎng)也能正常打開(kāi)。報(bào)告里有每個(gè)用例的耗時(shí)、狀態(tài)、失敗時(shí)的堆棧日常足夠用了。4.2 allure報(bào)告當(dāng)團(tuán)隊(duì)需要更多細(xì)節(jié)時(shí)熱詞里頻繁出現(xiàn)的pytest allure報(bào)告是另一個(gè)重量級(jí)選手。Allure的優(yōu)勢(shì)在于它能把每個(gè)步驟、附帶的請(qǐng)求參數(shù)、日志、截圖都結(jié)構(gòu)化地整合進(jìn)報(bào)告里適合做接口測(cè)試或UI測(cè)試的團(tuán)隊(duì)。用法也不復(fù)雜pip install allure-pytest pytest --alluredirallure-results allure serve allure-results--alluredir先輸出原始數(shù)據(jù)再用allure serve起一個(gè)本地Web服務(wù)展示報(bào)告。相比pytest-htmlAllure能更直觀地展示歷史趨勢(shì)和分類統(tǒng)計(jì)長(zhǎng)期維護(hù)的時(shí)候優(yōu)勢(shì)特別明顯。它的學(xué)習(xí)曲線主要在理解allure.feature、allure.story、allure.step這些裝飾器的組織方式建議從一個(gè)小模塊開(kāi)始試點(diǎn)別一上來(lái)全量鋪。4.3 pytest-xdist多核并行跑測(cè)試測(cè)試套件大了以后串行執(zhí)行的耗時(shí)是團(tuán)隊(duì)最直觀的痛點(diǎn)。pytest-xdist提供了無(wú)腦級(jí)別的加速pip install pytest-xdist pytest -n 4-n 4表示用4個(gè)并發(fā)worker。我實(shí)測(cè)下來(lái)一個(gè)400條用例的項(xiàng)目從6分鐘壓縮到2分鐘出頭效果非常顯著。但注意一點(diǎn)并行跑的前提是測(cè)試之間沒(méi)有共享可變狀態(tài)。如果你某些用例依賴同一個(gè)文件、同一個(gè)數(shù)據(jù)庫(kù)記錄并發(fā)時(shí)大概率隨機(jī)性的失敗。這類用例要先通過(guò)fixture的scope和tmp_path保證數(shù)據(jù)隔離否則加了并發(fā)會(huì)得到一堆莫名其妙的報(bào)錯(cuò)半夜還得被報(bào)警吵醒。4.4 pytest-cov覆蓋率到底夠不夠覆蓋率這個(gè)問(wèn)題經(jīng)常被誤解但還是要引入pytest-cov來(lái)度量哪怕只當(dāng)參考。安裝后運(yùn)行pytest --covsrc --cov-reporthtml會(huì)在命令行給出整體覆蓋率并生成一個(gè)htmlcov目錄點(diǎn)開(kāi)能看到每個(gè)文件里哪些行被覆蓋了、哪些漏掉了。覆蓋率數(shù)字高不代表測(cè)試寫得好但覆蓋率低一定說(shuō)明有大量路徑?jīng)]測(cè)到。我通常把核心模塊的覆蓋率目標(biāo)定在80%以上但更關(guān)注的是“關(guān)鍵分支和異常路徑”有沒(méi)有覆蓋到而不是糾結(jié)那幾行打印日志沒(méi)跑到。4.5 結(jié)合requests做接口測(cè)試的場(chǎng)景最后接上很多人的實(shí)際場(chǎng)景用Pytest做接口自動(dòng)化。其實(shí)不需要什么特別復(fù)雜的庫(kù)requests加上Pytest本身就夠用再加一個(gè)pytest.ini里統(tǒng)一配置BASE_URL接口測(cè)試就能跑得明明白白。常見(jiàn)的做法是fixture里創(chuàng)建session、維護(hù)token然后通過(guò)參數(shù)化去覆蓋各種入?yún)⒔M合。UI自動(dòng)化那邊Pytest和Selenium、Playwright的配合也順理成章因?yàn)閒ixture可以很好地管理瀏覽器實(shí)例的啟動(dòng)和關(guān)閉。熱詞里還有人問(wèn)pytest pycharm其實(shí)就是在IDE里跑這些套件的配置前面已經(jīng)講過(guò)了。5. 常見(jiàn)問(wèn)題與排查技巧5.1 中文路徑與編碼問(wèn)題Windows環(huán)境下項(xiàng)目路徑帶中文時(shí)Pytest偶爾會(huì)報(bào)編碼相關(guān)的錯(cuò)誤。這個(gè)問(wèn)題的根源其實(shí)是Python默認(rèn)讀取配置和測(cè)試文件名時(shí)用了系統(tǒng)api而系統(tǒng)和終端編碼不一致。解決辦法是在pytest.ini里加一行[pytest] ...如果還不行檢查系統(tǒng)區(qū)域的UTF-8支持Windows設(shè)置里把“beta版使用Unicode UTF-8提供全球語(yǔ)言支持”打開(kāi)重啟后再試。我在團(tuán)隊(duì)里遇到過(guò)不下三次這種問(wèn)題解決起來(lái)其實(shí)就這么簡(jiǎn)單。5.2 測(cè)試收集不到用例明明寫了test文件剛用Pytest的人最常遇到“明明寫了測(cè)試文件運(yùn)行卻顯示no tests ran”。逐個(gè)排查這三件事文件名是否滿足test_*.py模式里面的測(cè)試函數(shù)名是否以test開(kāi)頭pytest.ini里的testpaths是否指向了正確目錄第三種情況是我踩坑最多的。testpaths寫錯(cuò)之后Pytest會(huì)直接忽略你的測(cè)試目錄而且不報(bào)錯(cuò)它只會(huì)覺(jué)得“沒(méi)找到用例”。我建議第一輪無(wú)論如何先把testpaths刪了測(cè)試一下確認(rèn)文件能發(fā)現(xiàn)再慢慢加上配置。5.3 fixture名字沖突問(wèn)題fixture多了之后conftest.py里同名fixture可能覆蓋另一個(gè)文件里的本地同名fixturePytest的fixture解析順序是“最近的優(yōu)先”。這個(gè)問(wèn)題有時(shí)會(huì)鬼魅地導(dǎo)致“本地定義了fixture但執(zhí)行時(shí)用的卻是另一個(gè)”。排查方法很簡(jiǎn)單給fixture起名時(shí)帶上模塊前綴比如api_client改成user_api_client不要用data這種爛大街的名字。也可以用pytest --fixtures命令列出當(dāng)前所有的fixture定義直接看實(shí)際生效的是哪個(gè)。5.4 斷言失敗的堆??床欢芏嘈律鲜值耐瑢W(xué)看到一大片DAG追蹤就慌了。實(shí)際上Pytest在-v模式下失敗信息已經(jīng)通過(guò)assert的表達(dá)式分析給出了最關(guān)鍵的對(duì)比大段堆棧通常只是調(diào)用鏈。記住一條紀(jì)律斷言失敗時(shí)先看信息里的assert這一行和下面的對(duì)比值不要從頭開(kāi)始讀堆棧。如果你覺(jué)得信息還不夠可以自己在fixture或被測(cè)函數(shù)入口打日志配合-s輸出很快就能定位。5.5 運(yùn)行順序與隨機(jī)性問(wèn)題在unittest里用例的執(zhí)行順序通常按名稱排序Pytest默認(rèn)也是這樣。但很多測(cè)試其實(shí)不該依賴順序。一旦你依賴“某個(gè)用例先跑、某個(gè)用例后跑”你就在給自己埋雷。比如某個(gè)用例依賴另一個(gè)用例寫入的數(shù)據(jù)一旦加并發(fā)或者調(diào)整執(zhí)行順序整套測(cè)試就崩。解決方案是把數(shù)據(jù)準(zhǔn)備收斂到fixture里誰(shuí)需要誰(shuí)就聲明而不是靠執(zhí)行順序傳遞。在本地調(diào)試時(shí)可以用pytest-randomly這類插件打亂順序運(yùn)行盡早發(fā)現(xiàn)順序依賴。5.6 不要為了復(fù)用而過(guò)度設(shè)計(jì)最后這條算是我自己的體會(huì)。很多人寫測(cè)試框架的時(shí)候容易上頭一上來(lái)就是抽象類、工廠模式、自定義裝飾器連斷言都要封裝一層。結(jié)果測(cè)試代碼的復(fù)雜度遠(yuǎn)超業(yè)務(wù)代碼用例出了問(wèn)題時(shí)排查成本反而更高。Pytest本身已經(jīng)幫你做好了復(fù)用和擴(kuò)展的底座你要做的更多是保持簡(jiǎn)單。優(yōu)先用內(nèi)置的fixture、參數(shù)化、marker去組織等確確實(shí)實(shí)出現(xiàn)重復(fù)且固定的需求再去考慮更上層的封裝。我在實(shí)際帶項(xiàng)目時(shí)有個(gè)經(jīng)驗(yàn)測(cè)試代碼的評(píng)審標(biāo)準(zhǔn)應(yīng)該和業(yè)務(wù)代碼一樣嚴(yán)格。命名、可讀性、職責(zé)單一都要遵守。你寫在測(cè)試?yán)锏拿恳粋€(gè)魔法數(shù)字、每一個(gè)“暫時(shí)這樣”、每一段無(wú)注釋的復(fù)雜邏輯都會(huì)在三個(gè)月后變成你和同事的噩夢(mèng)。Pytest只是讓這個(gè)過(guò)程更舒服它替代不了工程紀(jì)律。如果讓我給新手一條最值得的建議那就是先把fixture和參數(shù)化這兩樣?xùn)|西吃透它們能解決你80%的測(cè)試組織問(wèn)題。我見(jiàn)過(guò)太多人把精力耗在研究各種花哨插件上最后fixture都沒(méi)用明白這是本末倒置。測(cè)試的意義在于給你信心讓你敢改業(yè)務(wù)代碼而不是給你一套運(yùn)行了卻沒(méi)人敢依賴的儀式。讓自己的套件始終保持著“跑起來(lái)很快、掛了能秒懂、換環(huán)境幾分鐘就能復(fù)現(xiàn)”的狀態(tài)這比任何技術(shù)選型都重要。