Python 單元測試
Test your software, or your users will. "Test ruthlessly. Dont make your users find bugs for you."
最近看了Axb的自我修養寫的關於好代碼,爛代碼和單元測試的一些文章,挺受啟發的,結合python講一下自己對單元測試的理解和操作。
單元測試是什麼
單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟體設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。
為什麼要進行單元測試? 測試除了保證程序的健壯性外,是可以讓你重新思考代碼的設計的。引用Axb博客的話:
- 編寫單元測試的難易程度能夠直接反應出代碼的設計水平,能寫出單元測試和寫不出單元測試之間體現了編程能力上的巨大的鴻溝。無論是什麼樣的程序員,堅持編寫一段時間的單元測試之後,都會明顯感受到代碼設計能力的巨大提升。
如果發現代碼難以構造測試,很有可能就是介面設計不夠優雅,或者耦合嚴重,嘗試從測試的角度思考能夠讓我們更好地設計。單元測試同時也為重構提供了保證,比如我們想優化一個函數內部實現,更換更優的數據結構和演算法,只需要重新跑一下測試就可以驗證新的實現是否引入了錯誤或bug。
總的來說,單元測試有以下好處:
- 確保代碼質量
- 改善代碼設計,難以測試的代碼一般是設計不夠簡潔的代碼。
- 保證重構不會引入新問題,以函數為單位進行重構的時候,只需要重新跑測試就基本可以保證重構沒引入新問題。
測試如何影響代碼設計
以上來自《編寫可讀代碼的藝術》,需要自己實踐才有體會。
python測試相關庫
- unittest,內置庫,模仿PyUnit寫的,簡潔易用,缺點是比較繁瑣。
- nose,測試發現,發現並運行測試。
- pytest,筆者目前喜歡用這個,寫起來很方便,並且很多知名開源項目在用,推薦。
- mock, 替換掉網路調用或者 rpc 請求等
使用pytest進行python進行單元測試
python內置了一個unittest,但是寫起來稍微繁瑣,比如都要寫一個TestCase類,還得用 assertEqual, assertNotEqual等斷言方法。 而使用pytest運行測試統一用assert語句就行,兼容unittest,目前很多知名開源項目如PyPy,Sentry也都在用。關於pytest的使用可以參考其官方文檔,雖然有很多高級特性,但是掌握其中一小部分基本就夠用了。
下面是py.test的基本用法,以常見的兩種測試類型(驗證返回值和拋出異常)為例:
def add(a, b):n """return a + bnn Args:n a (int): intn b (int): intnn Returns:n a + bnn Raises:n AssertionError: if a or b is not integernn """n assert all([isinstance(a, int), isinstance(b, int)])n return a + bnnndef test_add():n assert add(1, 2) == 3n assert isinstance(add(1, 2) , int)n with pytest.raises(Exception): # test exceptionn add(1, 2)n
這是個腦殘示例,不過基本使用就是這麼簡單。真實場景下遠遠比這個複雜,甚至有時候構造測試的時間比寫業務邏輯的時間還要長。但是再複雜的邏輯也是一點點功能堆積,如果可以確保每一部分都正確,整體上是不會出錯的。單元測試同時也提醒我們,函數完成的功能儘可能單一,這樣才利於測試。
下面幾個是我常用的pytest命令:
py.test test_mod.py # run tests in modulenpy.test somepath # run all tests below somepathnpy.test -q test_file_name.py # quite輸出npy.test -s test_file_name.py # -s參數可以列印測試代碼中的輸出,默認不列印,print沒結果npy.test test_mod.py::test_func # only run tests that match the "node ID",npy.test test_mod.py::TestClass::test_method # run a single method inn
測試驅動開發(TDD)的流程
為了實現一個函數,很多人的流程是這樣的:
匆匆寫代碼->實現後print輸出看結果->有邏輯或語法錯誤->修改->繼續print輸出看結果 循環往複。
採用TDD的流程如下:
TDD三項法則:
- 在編寫失敗的單元測試之前,不要編寫任何產品代碼
- 只要有一個單元測試失敗了,就不再寫測試代碼;
- 產品代碼能夠讓當前失敗的單元測試成功通過即可,不要多寫
優點:確定性;大幅減少缺陷;增加重構勇氣;單元測試即是文檔;改善設計 (事後測試是防守,先行測試是進攻) 當然也不用完全採用tdd,先寫測試有時候很繁瑣,但是對於比較重要的api函數,最好還是要有單元測試。為了使項目質量得到保證,TDD中的一些思想還是值得借鑒的。很多東西也在摸索,推薦學習下flask,requests等開源項目的單元測試代碼,以後再慢慢更新吧。 實際上,如果能把print的結果和預期結果落實到測試代碼里,加幾個assert語句,就是單元測試了。並且這些測試代碼也成為很好的api手冊,你看這些測試用例就知道怎麼調用了。
TDD實踐
之前做新項目的時候(使用了flask cookiecutter生成項目模板),基本上達到了凡是複雜函數或類都會寫測試代碼的程度。基本上都是用py.test,還是比較方便的。在項目跟目錄下建立一個tests文件夾,相關測試代碼都放在裡邊。之後我會裝一個用來監控文件變化的命令,我會使用vim的分屏模式同時打開模塊代碼和其測試代碼,然後開個tmux窗口用於邊改代碼邊看測試結果輸出,屏幕夠寬的話一個屏幕就能搞定。例如:假如我在寫一個模塊叫做models.py,在tests里寫個test_models.py,還有個很簡單的shell腳本runtest.sh寫上py.test -s test_models.py 首先裝一下監控文件變動的命令:
# for ubuntunsudo pip install https://github.com/joh/when-changed/archive/master.zipn# for macnbrew install fswatch # http://stackoverflow.com/questions/1515730/is-there-a-command-like-watch-or-inotifywait-on-the-macn
然後在tests文件夾下執行:
# ubuntunwhen-changed test_models.py ./runtest.shn# macnfswatch -o ./*.py | xargs -n1 ./runtest.sh # 比如寫單元測試的時候修改後就讓測試執行n
這樣就能非常愉快地邊改代碼和測試(實際上一定程度上可以說是TDD,只不過我有時候後寫測試代碼,另外我也在嘗試TDD是否真的能夠提升代碼質量並且不會降低開發效率,凡事只有自己試試才知道,過幾個月我再繼續更新本片的實踐,到時候就知道TDD到底是不是在浪費時間),邊看單元測試的執行結果啦:).如果遵守pep8寫不超過80列的代碼的話,即使用mac air這種小屏幕依然可以改得很爽,效果如下:
如何處理測試中的資料庫請求和網路請求?
之前有同學問到如何處理和資料庫的交互以及網路請求,結合自己之前寫單測的一些經驗說一下:
- 處理資料庫請求:目前我看到有兩種方式。無論使用那種方式,盡量保證數據測試的時候插入,使用完成就銷毀。這樣換個平台依然很容易構造測試 ,也容易在 CI 系統跑。
- 使用 fixture 類裝飾器在一個 TestCase 運行前插入數據到測試資料庫。大概就是 fixture 接收一個參數 sql 文件名,然後讀取數據插入資料庫
- 在 TestCase 的 setup 里插入數據,在 teardown 里銷毀。
@fixture(table.sql)nclass SomeTestCase:n passnnclass SomeTestCase:n def setUp(self):n # insert valuen def tearDown(self):n # destroy valuen
- 處理外部網路調用。依舊有兩種方式
- stub: 用來處理一些比較通用的請求,比如一個發號器代碼
- mock: 使用最多的替換掉網路請求的方式,幾乎所有場景下都可以用。個人推薦所有網路請求和 rpc 調用等都可以用 mock.patch 來模擬返回值
@registry.stubnclass ZoneSeqStub(BaseStub):n def id(self):n return zone:///seqdnn @stub(Seq.get_id)n def get_id(self, **kwargs):n return random.randint(1, 100)nnn class TestCase:n @mock.patch(somemodule.request)n def test_function(self, mock_request):n mock_request.return_value = {} # 構造期望的返回值,我們默認外部調用按照約定是可以工作的,不會對其測試n
推薦閱讀:
※完全理解Python關鍵字"with"與上下文管理器
※爬蟲入門到精通-headers的詳細講解(模擬登錄知乎)
※【掃盲】五分鐘了解Python
※Python 3.6全揭秘