[Pytest 101] 02 Mock 的基本用法
在 Pytest 中如何使用 Mock 來模擬行為、拋出例外與測試副作用。

介紹完基本的工具後,這篇文章要實際操作 Pytest 中 Mock 的使用方式。
Mock 可以讓我們替換原本的函式邏輯,模擬不同情境,寫出更精準的單元測試。
本文將以實例說明如何抽換函式、模擬錯誤發生,甚至處理像是 Redis 這樣的外部資源。
使用 mocker.patch
來抽換自身或是其他模組的函數
以抽換來自 service.my_service
的 do_something
函式為例
mocker.patch("service.my_service.do_something", return_value="hello")
Mock function
# my_project/service/do_service.py
def do_something():
try:
response = something()
return response
except Exception as e:
return str(e)
def something():
return "success"
抽換了 something()
這個函式,並且使用 return_value
定義其回傳值。
為了證明抽換的結果如預期,我們直接把值回傳,並且用 assert
斷言進行判斷。
# my_project/test/service/test_do_service.py
import pytest
from pytest_mock import MockFixture
from service.do_service import (
do_something,
)
def test_do_something_success(mocker: MockFixture):
success_message = "success"
mocker.patch("service.do_service.something", return_value=success_message)
result = do_something()
assert result == success_message
Mock function from other module
隨著專案複雜度增加,逐漸把共用函式統整成再一起是很常見的。
假設今天 something()
是來自模組 service/do_trick.py
在程式中會使用 import
引入來自 do_trick.py
的函式 something()
# my_project/service/do_trick.py
def something():
return "success"
# my_project/service/do_service.py
from service.do_trick import something
def do_something():
return something()
此時抽換的方法其實同小異,只有一點要特別注意:
當使用 from package_name import function_name
時,會將 function_name
視為當前模組的方法
所以要抽換方法的對象不再是原先的 module package_name
而是當前的模組 service.do_service.something
如果 mock package_name.function_name
則測試時會使用未抽換的方法
而因為呼叫的 function 已經被引入到當前的模組內,屬於 mock 了個寂寞
# my_project/test/service/test_do_service.py
import pytest
from pytest_mock import MockFixture
from service.do_service import (
do_something,
)
def test_do_something(mocker: MockFixture):
mocker.patch("service.do_service.something", return_value="not success")
assert do_something() == "not success"
在這個例子中就是抽換了 service.do_service
內的 something()
,而不是 service.do_trick
的函式。
Mock try-except
當有 try-except 時,也可以抽換方法來模擬某個地方出錯造成 except
後的行為。
使用 side_effect
宣告 something()
在執行時拋出錯誤類別 Exception
def test_do_something_error(mocker: MockFixture):
error_message = "Exception error"
mocker.patch("service.do_service.something", side_effect=Exception(error_message))
result = do_something()
assert result == error_message
return_value
只能定義「回傳值」,而沒辦法做更複雜的使用
而 side_effect
可以帶入 function, iterable 以及常用到的 exception class
。
Mock raise Exception
有時候是程式執行沒有出錯,但是在特定條件時我們要主動拋出錯誤
# my_project/service/do_service.py
def do_something(err):
if err:
raise Exception(f"Something went wrong")
return "success"
這時候可以使用 with pytest.raises()
來聲明執行時會拋出的錯誤類型及訊息
# my_project/test/service/test_do_service.py
import pytest
from pytest_mock import MockFixture
from service.test_do_service import (
do_something,
)
def test_do_something_success():
assert do_something(err=False) == "success"
def test_do_something_err():
with pytest.raises(Exception, match=f"Something went wrong"):
do_something(err=True)
Mock Redis
因為
redis.get
會回傳bytes
所以需要用到encode
和decode
。
def get_from_redis(id:int):
# connect to redis
redis = Redis(host=os.environ["REDIS_HOST"])
byte_data = redis.get("mykey.{}".format(id))
data = byte_data.decode("utf-8")
# set key to redis and expire after 1 hour
redis.set(
"mykey.{}".format(id),
data,
ex=3600,
)
return data
def test_get_from_redis(mocker: MockFixture):
test_data = "test_data".encode("utf-8")
mock_redis = mocker.patch("service.do_service.Redis")
mock_redis_instance = mock_redis.return_value
mock_redis_instance.get.return_value = test_data
mock_redis_instance.set.return_value = None
result = get_from_redis(1)
assert result == test_data.decode("utf-8")