python單元測試之 mock.patch 與 os.listdir
前言:
這篇主要跟os.listdir沒有太大的關聯,主要是講我在單元測試時遇到的問題與解決的方法,但是我一開始以為問題出在os.listdir,把mock.patch跟os.listdir當作關鍵字去搜尋,意外的滿多人有一樣的狀況,於是也決定在標題補上os.listdir希望能幫到有一樣問題的人。
說明:
今天早上我打算開始一個全新的python項目,主要用來管理我的圖片,有鑒於自己前陣子在 PHP的另一項目嘗試單元測試的成果不錯,決定也來試看看python的單元測試,python內建單元測試的用法我相信網上已經有很多優質教學,我目前也只使用到必備的繼承unittest.TestCase跟假資料的mock.patch而已,這邊就不再多做教學,只列出兩個我在學習時覺得好吸收的文章。
unittest部分
https://imsardine.wordpress.com/tech/unit-testing-in-python/
mock部分
https://evangui.com/2016/02/29/%E7%94%A8-mock-%E4%BE%86%E4%BD%9C-python-unit-test/
文件說明:
ENV2是我的virtualenv虛擬環境
core中的Provider.py是要被測試的模組
tests中存放測試代碼,其命名規範依照規定使得discover指令可以自動測試
Provider內的PictureProvider Class是這次要被測試的主要對象,主要功能在於撈出特定目錄下的所有檔案,其中有用到PyFunctional這個套件,我個人相當推薦,可以像laravel的collection一樣串連操作。
PyFunctional:Python library for creating data pipelines with chain functional programming
測試部分就只貼出主要有問題的這段測試碼,我打算patch掉listdir跟isfile,讓我的測試跟外界資料隔絕開來。
但是失敗了!!從錯誤詢息可以看出listdir並沒有被patch掉,他正常執行後找不到testPath這個我測試的路徑而報錯。
開始除錯:
於是我開始懷疑listdir跟patch是不是有問題,我找到這篇文章
https://stackoverflow.com/questions/44572484/mocked-patch-of-os-listdir-not-working-for-unittesting
@mock.patch("path.to.your.pythonfile.listdir")
哇!這就是我要的!馬上加上去看看
@mock.patch('PictureProvider.listdir')
No module named,差點忘記patch要給完整Namespace
@mock.patch('core.provider.PictureProvider.listdir')
但是他還是錯了,does not have the attribute
於是我想到另一個可能性 ,換個方式試試看
我先將patch改回來os.listdir
@mock.patch('os.listdir')
還有
被測試的Class引用時改用import os,內部呼叫時用os.listdir呼叫
他動了,先忽視那個測試未通過,至少已經確認listdir有替換成功,他呼叫的是測試函式,問題大解決,但是這樣我要為了測試再也不能用from ... import了
所以我開始找為什麼換成from ... import就會替換不到我的listdir
最終我將被測試程式改回來
測試程式改為
然後
他動了,但是為什麼呢,查了很久後我找到python官網的import說明
Each module has its own private symbol table
Modules can import other modules. It is customary but not required to place all import statements at the beginning of a module (or script, for that matter). The imported module names are placed in the importing module’s global symbol table
>>> from fibo import fib, fib2
>>> fib(500)
1 1 2 3 5 8 13 21 34 55 89 144 233 377
This does not introduce the module name from which the imports are taken in the local symbol table (so in the example, fibo is not defined).
也就是說,當你import os時,需要os.listdir呼叫這個其他模組 ( 擁有不同命名空間 ) 的函式,但是當你from os import listdir時,python底層會把你要叫出的listdir加到你現在模組的裡面,此時你的命名空間下面就會多出listdir這個函式,內部呼叫的也是這個函式,而patch所吃的就是完整命名空間,所以我必須下
@mock.patch('core.provider.listdir')
patch掉我自己的模組下因為from os import listdir而增加的listdir函式,如果我
@mock.patch('os.listdir')
也會patch os.listdir成功,只是我的模組呼叫的是core.provider.listdir,兩個被判定為不一樣,所以測試才會失敗。
現在該回來看看測試結果為何不通過了
看來是join函式沒有成功
我一開始以為是join不支持不存在的路徑
但是試過之後是可以的
結果問題在
isfile也被我patch,所以不管你輸入什麼,他都會丟一樣的回傳值,甚至你丟的是需要執行的function也會直接被無視,導致我的join直接被吃掉不執行,所以改成
執行成功,痛哭流涕
結論:
整個早上踩了不少坑,不過最後不但達成了加入單元測試的目的,也更了解python的一些特性,算是不錯的體驗。
留言列表