昨天我們學會了參數化測試,用優雅的方式處理大量測試資料。今天要解決一個新挑戰:「如何測試依賴外部服務的函數?」
想像一個場景:你的應用需要呼叫 API、寄送 email 或讀取資料庫。在測試時,你不希望真的去呼叫這些外部服務。今天我們要學習「測試替身」,了解如何用假物件替換真實依賴。
今天結束後,你將學會:
mock
和 patch
用法第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期
├── Day 06 - 參數化測試
├── Day 07 - 測試替身基礎 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
測試替身(Test Double)是在測試中用來替代真實依賴的假物件。主要有兩種:
# 問題:直接依賴外部服務
import requests
class UserService:
def get_user_profile(self, user_id: int) -> dict:
response = requests.get(f"/api/users/{user_id}") # 真實 API 呼叫
user = response.json()
return {
'id': user['id'],
'name': user['name'],
'display_name': user['name'].upper()
}
測試時會遇到的問題:
Python 提供了強大的 unittest.mock 模組:
from unittest.mock import patch, Mock
# 建立 Mock
@patch('requests.get')
def test_with_mock(mock_get):
mock_get.return_value.json.return_value = {
'id': 1, 'name': 'John Doe', 'email': 'john@example.com'
}
建立 src/services/user_service.py
:
import requests
from typing import Dict, Any
class UserService:
def get_user_profile(self, user_id: int) -> Dict[str, Any]:
response = requests.get(f"/api/users/{user_id}")
user = response.json()
return {
'id': user['id'],
'name': user['name'],
'display_name': user['name'].upper()
}
def create_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
response = requests.post('/api/users', json=user_data)
return response.json()
建立 tests/day07/test_user_service.py
:
import pytest
from unittest.mock import patch, Mock
from src.services.user_service import UserService
class TestUserService:
def setup_method(self):
self.user_service = UserService()
@patch('src.services.user_service.requests.get')
def test_get_user_profile(self, mock_get):
# 設置 mock 回應
mock_response = Mock()
mock_response.json.return_value = {
'id': 1,
'name': 'John Doe',
'email': 'john@example.com'
}
mock_get.return_value = mock_response
result = self.user_service.get_user_profile(1)
assert result == {
'id': 1,
'name': 'John Doe',
'display_name': 'JOHN DOE'
}
# 驗證 API 被正確呼叫
mock_get.assert_called_once_with('/api/users/1')
@patch('src.services.user_service.requests.post')
def test_create_user(self, mock_post):
mock_response = Mock()
mock_response.json.return_value = {'id': 2, 'name': 'Jane'}
mock_post.return_value = mock_response
user_data = {'name': 'Jane', 'email': 'jane@example.com'}
result = self.user_service.create_user(user_data)
assert result == {'id': 2, 'name': 'Jane'}
mock_post.assert_called_once_with('/api/users', json=user_data)
@patch('src.services.user_service.requests.get')
def test_handles_api_error(self, mock_get):
mock_get.side_effect = requests.exceptions.RequestException("API Error")
with pytest.raises(requests.exceptions.RequestException):
self.user_service.get_user_profile(999)
@pytest.fixture
def mock_requests():
with patch('src.services.user_service.requests') as mock:
yield mock
def test_with_fixture(mock_requests):
mock_requests.get.return_value.json.return_value = {'id': 1, 'name': 'John'}
service = UserService()
result = service.get_user_profile(1)
assert result['display_name'] == 'JOHN'
@patch('src.services.user_service.requests.get')
def test_formats_user_name_to_uppercase(self, mock_get):
mock_get.return_value.json.return_value = {'id': 1, 'name': 'john'}
result = self.user_service.get_user_profile(1)
assert result['display_name'] == 'JOHN'
只 Mock 必要的部分,不要過度 Mock:
# ❌ 不好的做法:Mock 太多東西
@patch('src.services.user_service.UserService')
def test_over_mocking(self, mock_service):
# 連被測試的類別都 Mock 了
pass
# ✅ 好的做法:只 Mock 外部依賴
@patch('src.services.user_service.requests.get')
def test_minimal_mocking(self, mock_get):
# 只 Mock 外部 HTTP 請求
service = UserService() # 真實的服務物件
# ...
更新 tests/day07/test_user_service.py
:
import pytest
from unittest.mock import patch, Mock
import requests
from src.services.user_service import UserService
class TestUserServiceWithDoubles:
def setup_method(self):
self.user_service = UserService()
@patch('src.services.user_service.requests.get')
def test_returns_formatted_user_data(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {
'id': 1,
'name': 'John Doe',
'email': 'john@example.com'
}
mock_get.return_value = mock_response
result = self.user_service.get_user_profile(1)
assert 'id' in result
assert 'name' in result
assert 'display_name' in result
assert result['display_name'] == 'JOHN DOE'
mock_get.assert_called_once_with('/api/users/1')
@patch('src.services.user_service.requests.get')
def test_handles_different_user_names(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {
'id': 2, 'name': 'alice smith', 'email': 'alice@example.com'
}
mock_get.return_value = mock_response
result = self.user_service.get_user_profile(2)
assert result['display_name'] == 'ALICE SMITH'
@patch('src.services.user_service.requests.post')
def test_sends_correct_data_to_api(self, mock_post):
mock_response = Mock()
mock_response.json.return_value = {'id': 3, 'success': True}
mock_post.return_value = mock_response
user_data = {'name': 'New User', 'email': 'new@example.com'}
result = self.user_service.create_user(user_data)
assert 'success' in result
assert result['success'] is True
mock_post.assert_called_once_with('/api/users', json=user_data)
@patch('src.services.user_service.requests.get')
def test_handles_api_errors(self, mock_get):
mock_get.side_effect = requests.exceptions.RequestException("Network error")
with pytest.raises(requests.exceptions.RequestException):
self.user_service.get_user_profile(999)
今天我們深入學習了測試替身的重要概念:
測試替身是 TDD 中的重要工具,讓我們能夠:
記住:好的測試替身讓測試更快、更穩定、更專注。
明天我們將學習「例外處理測試」,了解如何驗證程式在異常情況下的行為。