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

對抗久坐職業傷害

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

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

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

贊助我們的創作

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

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