如何正確為歷史遺留代碼補充單元測試?
“
閱讀本文大概需要 5 分鐘。
我們知道,在軟件工程中,單元測試是保證軟件質(zhì)量的重要手段之一。一個優(yōu)秀的代碼,單元測試的代碼量,經(jīng)常會超過被測試的代碼本身。一個理想化的開發(fā)團隊,可能有三分之二的時間是在寫測試,剩下的三分之一時間才是寫業(yè)務(wù)代碼。
如果你的項目是從一開始就寫單元測試,那么你寫起來應(yīng)該輕松又愉快,因為單元測試會促使你的代碼自身變成可測試的代碼。
但如果你接手了一個大項目,里面已經(jīng)有幾十萬行代碼了,那么給這些代碼補單元測試會讓你知道什么叫做痛不欲生。你會發(fā)現(xiàn)有一些函數(shù),它讓你不知道怎么寫測試代碼。但你又不能隨便修改代碼的結(jié)構(gòu),誰知道會引起什么連鎖反應(yīng)?
我們來看一個例子:

我想測試的是business_code里面,check_data_dup分別返回 True 或者 False 的時候,下面代碼的邏輯。也就是說,我只關(guān)心第 18-27 行的邏輯。這個時候不關(guān)心 MySQL 和 Redis。但是每次測試都要從他們里面讀取數(shù)據(jù),這樣就會導(dǎo)致測試代碼依賴外部環(huán)境。如果 MySQL 或者 Redis 掛了,那么測試代碼就會運行失敗。
而且,就算 Redis 和 MySQL 沒有故障,你怎么知道你的 data_id 和 pk,在數(shù)據(jù)庫中對應(yīng)的是什么數(shù)據(jù)?為了分別走到特定的分支,你還需要去檢測數(shù)據(jù)庫中特定數(shù)據(jù)的 id。萬一是測試環(huán)境,別人修改了里面的數(shù)據(jù),你的測試也可能會掛掉。
如果直接使用 Pytest 來寫測試案例,代碼是這樣的:

可以看到,我運行 Pytest 以后,成功了一個,失敗了一個。這里我模擬出數(shù)據(jù)庫中沒有數(shù)據(jù)能夠讓check_data_dup走到返回True邏輯的情況。
難道為了讓單元測試進行下去,我還要去數(shù)據(jù)庫構(gòu)造一條特定的數(shù)據(jù)?這只是單元測試,又不是集成測試。
為了解決這個問題,我們就可以使用mock模塊。這是 Python 自帶的一個模塊,可以動態(tài)替換函數(shù)。
它的寫法非常簡單:

我們只需要使用@mock.patch裝飾器,裝飾測試函數(shù)就可以了。這個裝飾器接收兩個參數(shù),第一個參數(shù)是被模擬的函數(shù)的路徑,以點分割;第二個參數(shù)是你想讓它返回的值。
從上圖可以看到,test_runner.py運行以后,原本在read_data_from_redis和read_data_from_mysql中打印的兩段文字都沒有打印,說明這兩個函數(shù)已經(jīng)被動態(tài)替換了,他們內(nèi)部的代碼不會運行。只會直接返回我們預(yù)設(shè)的這個返回值。這樣一來就跟數(shù)據(jù)庫解耦了。
注意,在上圖中,由于我們已經(jīng) mock 了check_data_dup,因此read_data_from_redis和read_data_from_mysql兩個函數(shù)隨便返回什么值都可以。如果你想順帶也測試一下check_data_dup,那么可以不 mock 它,如下圖所示。

在check_data_dup函數(shù)的邏輯中,如果data參數(shù)含有字符x,并且user_id是偶數(shù),就返回True,否則返回False。我們通過 mock 兩個讀數(shù)據(jù)的函數(shù),分別設(shè)置不同的返回值,就能滿足讓check_data_dup返回不同值的條件。
mock.path有一個小坑,一定要注意。我們來看看下面這個文件結(jié)構(gòu):

read_data_from_redis和read_data_from_mysql兩個函數(shù)分布在了不同的文件里面。在runner.py中導(dǎo)入并使用了他們。test_runner.py中,我們使用@mock.patch對這兩個函數(shù)定義的路徑打補丁進行替換??墒翘鎿Q了以后,運行 Pytest,會發(fā)現(xiàn)這兩個函數(shù)竟然正常運行了。也就是說我們的替換失敗了。
之所以會出現(xiàn)這種情況,是因為我們要打補丁的并不是這兩個函數(shù)定義的地方,而是使用的地方。我們在runner.py中,分別使用如下兩個語句:
from?mysql_util.SqlUtil?import?read_data_from_mysql
from?controller.lib.redis.RedisUtil?import?read_data_from_redis
導(dǎo)入了這兩個函數(shù),我們也是在runner.py中使用他們的。因此,@mock.patch的第一個參數(shù),依然應(yīng)該是runner.read_data_from_redis和runner.read_data_from_mysql。
正確的做法如下圖所示:

mock.patch還有更多高級用法,例如替換類,替換實例方法等等。可以在unittest.mock中找到他。從P ython 3.3 開始,官方自帶了unittest.mock,它跟直接import mock的效果是一樣的。

End
崔慶才的新書《Python3網(wǎng)絡(luò)爬蟲開發(fā)實戰(zhàn)(第二版)》已經(jīng)正式上市了!書中詳細介紹了零基礎(chǔ)用 Python 開發(fā)爬蟲的各方面知識,同時相比第一版新增了 JavaScript 逆向、Android 逆向、異步爬蟲、深度學(xué)習(xí)、Kubernetes 相關(guān)內(nèi)容,?同時本書已經(jīng)獲得 Python 之父 Guido 的推薦,目前本書正在七折促銷中!
內(nèi)容介紹:《Python3網(wǎng)絡(luò)爬蟲開發(fā)實戰(zhàn)(第二版)》內(nèi)容介紹

掃碼購買

點個在看你最好看

