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.mockpatch 幫忙。

patch 能夠幫助我們仿製一個 object 及設定其回傳值,所以可以利用 patch 仿製 requests.get 並且設定回傳一個假的 object 只包含 status_codetext 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

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!