[Pytest 101] 04 Mock Class
介紹兩種在使用工廠模式時測試 class 建構的方式,並說明如何有效抽換依賴。

在開發中導入工廠模式雖然能讓架構更清晰,但也讓單元測試變得複雜。
一次搞懂兩種不同 mock 策略的優劣與實作方式,幫助你寫出更穩健的測試程式!
當程式碼中使用工廠模式(Factory Design Pattern)分別建構不同類別回傳時,有兩種單元測試的寫法。
# my_project/service/alert_service.py
from service.alert.mail_alert import MailAlert
from service.alert.teams_alert import TeamsAlert
from service.alert.discord_alert import DiscordAlert
def get_alert_model(session, alert_type, alert_config):
if alert_type == "mail":
return MailAlert(session, alert_config)
elif alert_type == "teams":
return TeamsAlert(session, alert_config)
elif alert_type == "discord":
return DiscordAlert(session, alert_config)
else:
raise Exception(
f"Alert Type is not valid. Please check the alert type mail, teams or discord. {alert_type}"
)
Mock class
因為已經 import 到 Module 中,所以抽換的對象是要測試的檔案中的類別
- service.alert_service.MailAlert
- service.alert_service.TeamsAlert
- service.alert_service.DiscordAlert
# my_project/test/service/test_alert_service.py
import pytest
from pytest_mock import MockFixture
from service.alert_service import (
get_alert_model,
)
def test_get_alert_model_success(mocker: MockFixture):
mock_session = mocker.MagicMock()
alert_config = {}
alert_type_model_dict = {
"mail": "MailAlert",
"teams": "TeamsAlert",
"discord": "DiscordAlert",
}
for alert_type, alert_model in alert_type_model_dict.items():
mock_alert_model = mocker.patch(f"service.alert_service.{alert_model}")
result = get_alert_model(mock_session, alert_type, alert_config)
assert result == mock_alert_model.return_value
mock_alert_model.assert_called_once_with(mock_session, alert_config)
def test_get_alert_model_invalid_type(mocker: MockFixture):
mock_session = mocker.MagicMock()
alert_type = "test_type"
with pytest.raises(
Exception,
match=f"Alert Type is not valid. Please check the alert type mail, teams or discord. {alert_type}",
):
get_alert_model(mock_session, alert_type, {})
說明
先使用 mocker.patch
將對應的類別 mock 出來成 mock_alert_model
物件
再用斷言式判斷函式得到的結果與 mock_alert_model
相同
最後使用 assert_called_once_with
確認 mock_alert_model 只被呼叫一次,且該次呼叫使用了指定的引數
更多可用的斷言方法:使用 Mock 的方式
引入實際類別
另一種方法是在測試中 import 類別對真實類別產生依賴。
這樣做可以更貼近實際執行的狀況,但是測試的撰寫會更加複雜。
此方法可以使用 isinstance
來判斷類別,當引入的類別被修改時可能會對測試案例產生影響。
並且如果傳入的測試資料要無法建立對應的類別,會直接報錯。
以這邊來說就是如果建構類別時會使用到 alert_config
中的參數 alert_content
而我們傳入的是空 dict
的時候就會發生錯誤:
AttributeError: ‘dict’ object has no attribute ‘alert_content’
# my_project/test/service/test_alert_service.py
import pytest
from pytest_mock import MockFixture
from service.monitor.alert.mop_mail_alert import MailAlert
from service.monitor.alert.teams_alert import TeamsAlert
from service.monitor.alert.trigger_airflow_dags import DiscordAlert
from service.alert_service import (
get_alert_model,
)
def test_get_alert_model_success(mocker: MockFixture):
mock_session = mocker.MagicMock()
alert_config = {}
# 使用真實類別
alert_type_model_dict = {
"mail": MailAlert,
"teams": TeamsAlert,
"discord": DiscordAlert,
}
# 不對類別進行 mock
for alert_type, alert_model in alert_type_model_dict.items():
result = get_alert_model(mock_session, alert_type, alert_config)
assert isinstance(result, alert_model)