[Python] 測試雙刀,unittest 和 mock

Bryan Yang
A multi hyphen life
6 min readJan 20, 2018

測試做得好,要飯要到老

測試應該是軟體工程師必備的技能之一,雖然比起 TDD ,我更偏向 ATDD (https://en.wikipedia.org/wiki/Acceptance_test%E2%80%93driven_development),但是不管怎麼說,測試是一種確保軟體品質以及可重構性的手段.

雖然有些人會說寫 Test 也不能保證程式不會錯,但是這就像就算戴全罩安全帽也有可能出車禍一樣,全罩安全帽還是提供了比西瓜皮更多保護,雖然不能擋住所有衝擊,但是在相同的衝擊底下,全罩安全帽還是比西瓜皮安全.

本篇沒有想多說怎麼做測試,以及要做哪些測試,而是先介紹兩個假使想做測試,一定要知道的套件和基本使用方法.

Unittest

Python 自帶的測試工具,以前用 unittest2 現在用 unittest 是有點差但是大致上差不多,常用的就那幾個.

範例(按照TDD 寫法應該是先寫 test ,這邊為了說明方便,先放 function):

foo.py

def add_int(x, y):
if isinstnace(x, int) and isinstance(y, int):
return x + y
else:
raise ValueError("{} or {} is not Integer.".format(x, y))

一個非常簡單的 add function,一開始先檢查兩個 input 是不是整數,如果是就相加,任何一個不是就吐 ValueError

test_foo.py

import unittest
import foo
class TestFoo(unittest.TestCase):
def test_add_two_int(self):
result = foo.add_int(1,2)
self.aseertEquals(result, 3)

一個簡單的 unittest 當然先 import unittest(廢話)和剛剛寫好的 module.起一個 class 繼承 unittest.TestCase,接著就可以寫測試方法.寫測試方法幾個小點:

  1. 方法的命名最好一看就知道在測什麼,不然要多寫註解.
  2. 一次測一個方法,一種組合.例如這個測試就只會測 add 這個方法在輸入兩個值都為整數的情況.如果要測其他狀況就要另外寫.

Mock

當一個方法裡面如果不只單一功能,還會使用到其他方法時,為了確保只測試這個單一的方法,就會把外部方法 Mock 起來,一來是測試邏輯乾淨,二來是很好模擬各種回傳狀況.

例如下面這個狀況
api.py

import hug
from aaa import check_status
@hug.get('/status')
def status(response):
try:
task = check_status.delay()
if task:
return 'ok'
else:
response.status = HTTP_500
except Exception as e:
response.status = HTTP_404

這個 API Call 相當單純,呼叫 check_status.delay(),根據回傳結果做不同的回覆.因為是做 unittest,我們只想聚焦在 status 的回傳邏輯,不需要真的去觸發 check_status_delay(),這時候就需要使用mock.

import unittestimport hug
from falcon import HTTP_200, HTTP_500
from mock import patchimport api
from aaa import check_status
class TestAPI(unittest.TestCase): def test_check_status(self):
with patch('aaa.check_status.delay') as check:
check.return_value = 'foo'
res = hug.test.get(api, 'status')

check.assert_called_once()
self.assertEquals(res.status, HTTP_200)

首先因為 api 我是用 hug (https://github.com/timothycrosley/hug)這個框架,他有提供簡單測試的方法,所以先 import 近來.接著 import mock 其中的 patch,要測試 api ,以及要被 mock 的 check_status.

進入 test_check_status 方法.第一行 with patch(‘aaa.check_status.delay’) as check 是 mock.patch 核心用法,把原本 check_status.delay 變成用 check 接管,在 with 內的程式,只要執行到 check_status.delay,都會變成呼叫 check.因此在這裡就可以自由控制 check_status.delay 的輸出結果.

下一行的 check.return_value = ‘foo’ 表示當我們在執行 hug.test.get(api, ‘status’) 時,就會觸發原本 api.py 中的 status 方法,其中 check_status.delay() 的結果就會變成 ‘foo’.因此會 return ‘ok’ 回來.

為了確認 check 真的有被執行到,mock 物件有提供 assert_called_once() 來檢驗程式有被呼叫到一次(只有一次,如果沒有呼叫或是執行兩次以上都會報錯.)

另外由於沒有任何異常狀況,response 的 status 是 200,可以透過 self.assertEquals(res.status, HTTP_200) 來做檢驗.

另外為了驗證另外一種狀況,需要再多寫一條 test:

    def test_check_status_id_not_found(self):        with patch('aaa.check_status.delay') as check:
check.return_value = None
res = hug.test.get(api, 'status')
check.assert_called_once()
self.assertEquals(res.status, HTTP_500)

前面過程都一樣,差在 check.return_value = None 的回傳值.由於回傳值是 None,最後的 api 應該要吐 HTTP_500 給我.

從上面的例子可以看到,透過 Mock 可以輕易隔離 side effect ,方便測試單一方法以及內部邏輯.同時也由於 mock 多起來也是很麻煩,在寫程式的時候會提醒自己盡量避免有太多的 side effect 在一個方法中(寫 api 時滿容易遇到這樣的問題.)

--

--

Bryan Yang
A multi hyphen life

Data Engineer, Data Producer Manager, Data Solution Architect