Python 測試利器 unittest.mock.patch
Posted on Sep 8, 2018 in Python 程式設計 - 中階 by Amo Chen ‐ 3 min read
自 Python 3.3 增加 unittest.mock 函式庫之後,測試 Python 程式又更方便了。
不過,剛接觸測試相關技術的初學者應該會對 unittest.mock 感到困惑,本篇就解說 unittest.mock 中的 patch 功能,作為學習, unittest.mock 的 Hello World.
mock 有仿製品的意思,所以從字面上來看 mock 的功用其實就是提供仿製的功能。
那仿製什麼呢? mock 可以仿製程式裡面的 object, 讓我們可以在測試時利用仿製的 object 抽換掉某些 object.
在此先舉一個網頁爬蟲(crawler)程式為例,稍後接著說明如何利用 mock 仿製抽換程式裡特定的部分,以達到測試的效果。
p.s. 請先安裝以下套件(本文使用 python 3.6.5)
$ pip install requests lxml
網頁爬蟲範例程式( crawler.py 此程式會擷取網頁標題,也就是網頁原始碼中 <title></title> 的部分):
# -*- coding: utf-8 -*-
import requests
import lxml.html
def fetch_url_title(url):
    resp = requests.get(url)
    if resp.status_code == 200:
        html = lxml.html.fromstring(resp.text)
        return html.xpath('//title')[0].text
    return 'unknown'
if __name__ == '__main__':
    title = fetch_url_title('https://www.google.com.tw/')
    print(title)
上述範例程式的測試程式碼( test_crawler.py )可能會是以下形式:
# -*- coding: utf-8 -*-
import unittest
from crawler import fetch_url_title
class TestCrawler(unittest.TestCase):
    def test_fetch_url(self):
        assert 'Google' == fetch_url_title('https://www.google.com.tw/')
上述測試會實際對 Google 發出 HTTP request ,並取得實際的網頁資料,可以輸入以下指令進行測試:
$ python -m unittest test_crawler.py
然後,實際對 Google 發出 HTTP request 其實會花費不必要的網路資源浪費,而且假設 Google 擋住我們的連線,就會讓測試失敗,如此一來還得特定花時間查看是否有網路或者 Google 問題,這樣並不是這個測試的真正目的。在這個測試中,我們並不需要關注 requests.get 是否能夠正常運作,而是要測試 lxml 能不能正確擷取網頁中的標題,所以在這情況下,我們可以 mock (仿製) requests.get ,並在測試中抽換掉它,如此一來,我們便能每次測試都確保 requests.get 都不會對 Google 真實發出請求,節省網路資源並確保測試的正確性。
這時候就可以請出 unittest.mock 的 patch 幫忙。
patch 能夠幫助我們仿製一個 object 及設定其回傳值,所以可以利用 patch 仿製 requests.get 並且設定回傳一個假的 object 只包含 status_code 及 text 2 個屬性。
可以先用以下程式玩看看:
>>> from unittest.mock import patch
>>> import requests
>>>
>>> class Resp(object): pass
>>>
>>> resp = Resp()
>>> resp.text = '<html><title>Google</title></html>'
>>> resp.status_code = 200
>>>
>>> patcher = patch('requests.get', return_value=resp)
>>> patcher.start()
>>>
>>> resp = requests.get('https://www.google.com.tw/')
>>> resp
<__main__.Resp at 0x10fdd2320>
上述程式中,我們先做了一個假的 Resp instance ,並且指定 text, status_code 2 個屬性給 resp ,然後將之視為 patch 過後的 requests.get 的回傳值(也就是 patcher = patch('requests.get', return_value=resp) ),接著呼叫 patcher.start() 開始 patch 。
然後我們試著呼叫一次 requests.get('https://www.google.com.tw/') 就會發現其回傳值是我們剛剛仿製的 Resp 物件了。偉哉, patch !
所以先前的測試搭配 patch 就可以改寫成為:
# -*- coding: utf-8 -*-
import unittest
from unittest.mock import patch
from crawler import fetch_url_title
class Resp(object): pass
class TestCrawler(unittest.TestCase):
    def setUp(self):
        resp = Resp()
        resp.text = '<html><title>Google</title></html>'
        resp.status_code = 200
        self.patcher = patch('requests.get', return_value=resp)
        self.patcher.start()
    def test_fetch_url(self):
        assert 'Google' == fetch_url_title('https://www.google.com.tw/')
    def tearDown(self):
        self.patcher.stop()
可以看到改寫後的測試程式在 setUp 方法中先把 requests.get patch 起來,接著測試執行時所用到的就是 patch 過後的 requests.get ,最後再 tearDown 方法中關掉 patch ,關掉之後如果再次呼叫 requests.get 就會是原先的 requests.get 。
以上就是最入門的 patch 用法了,建議後續可以閱讀官方文件 unittest.mock — mock object library ,裡面有更多關於 mock 的說明與範例。
Reference
https://docs.python.org/3/library/unittest.mock.html