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