Skip to content

Mock 和测试替身 ​

Mock 隔离外部依赖(数据库、API、文件系统),让测试快速、可靠、可控。

为什么需要 Mock ​

问题:测试依赖外部服务:

python
import requests

def get_weather(city):
    response = requests.get(f"https://api.weather.com/v1/{city}")
    return response.json()

# 测试问题:
# - 依赖网络
# - API 可能宕机
# - 响应不可控
# - 测试慢

解决:用 Mock 替代真实 API。

unittest.mock 基础 ​

Mock 对象 ​

python
from unittest.mock import Mock

# 创建 Mock
mock_api = Mock()

# 设置返回值
mock_api.get_data.return_value = {"temperature": 25}

# 调用
result = mock_api.get_data()
print(result)  # {"temperature": 25}

# 验证调用
mock_api.get_data.assert_called_once()

patch 装饰器 ​

替换模块中的对象:

python
from unittest.mock import patch
import requests

def get_weather(city):
    response = requests.get(f"https://api.weather.com/v1/{city}")
    return response.json()

@patch('__main__.requests.get')
def test_get_weather(mock_get):
    # 设置 Mock 响应
    mock_get.return_value.json.return_value = {"temp": 25}
    
    result = get_weather("Beijing")
    
    assert result["temp"] == 25
    mock_get.assert_called_once_with("https://api.weather.com/v1/Beijing")

MagicMock ​

支持魔术方法的 Mock:

python
from unittest.mock import MagicMock

mock_list = MagicMock()
mock_list.__len__.return_value = 5
mock_list.__getitem__.return_value = 42

print(len(mock_list))  # 5
print(mock_list[0])    # 42

何时使用 Mock ​

✅ 应该 Mock ​

  • 外部 API:HTTP 请求、第三方服务
  • 数据库:慢、需要设置
  • 文件系统:避免实际 I/O
  • 随机性:时间、随机数
  • 慢操作:sleep、重计算

❌ 不应该 Mock ​

  • 被测试的代码本身
  • 简单的数据结构(list、dict)
  • 纯函数(无副作用)

实战案例:模拟文件系统 ​

python
# file_processor.py
def read_config(filename):
    with open(filename) as f:
        return f.read()

def process_file(filename):
    content = read_config(filename)
    return content.upper()

# test_file_processor.py
from unittest.mock import patch, mock_open

def test_process_file():
    mock_data = "hello world"
    
    with patch("builtins.open", mock_open(read_data=mock_data)):
        result = process_file("config.txt")
    
    assert result == "HELLO WORLD"

实战案例:模拟网络请求 ​

python
# weather_service.py
import requests

class WeatherService:
    def get_temperature(self, city):
        response = requests.get(f"https://api.weather.com/{city}")
        response.raise_for_status()
        data = response.json()
        return data["temperature"]

# test_weather_service.py
import pytest
from unittest.mock import patch, Mock
import requests

@patch('weather_service.requests.get')
def test_get_temperature_success(mock_get):
    # 模拟成功响应
    mock_response = Mock()
    mock_response.json.return_value = {"temperature": 25}
    mock_get.return_value = mock_response
    
    service = WeatherService()
    temp = service.get_temperature("Beijing")
    
    assert temp == 25

@patch('weather_service.requests.get')
def test_get_temperature_error(mock_get):
    # 模拟网络错误
    mock_get.side_effect = requests.ConnectionError("Network error")
    
    service = WeatherService()
    with pytest.raises(requests.ConnectionError):
        service.get_temperature("Beijing")

pytest-mock ​

pytest 的 Mock 插件:

bash
pip install pytest-mock

使用 mocker fixture:

python
def get_user_name(user_id):
    import requests
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()["name"]

def test_get_user_name(mocker):
    # 使用 mocker.patch 替代 @patch
    mock_get = mocker.patch('requests.get')
    mock_get.return_value.json.return_value = {"name": "Alice"}
    
    result = get_user_name(123)
    assert result == "Alice"

优点:

  • 自动清理
  • 与 pytest fixtures 集成
  • 更简洁的语法

Mock 的常用方法 ​

return_value ​

python
mock = Mock()
mock.method.return_value = 42
print(mock.method())  # 42

side_effect ​

python
# 抛出异常
mock.method.side_effect = ValueError("Error")

# 多次调用不同返回值
mock.method.side_effect = [1, 2, 3]
print(mock.method())  # 1
print(mock.method())  # 2

assert 方法 ​

python
mock = Mock()
mock.method(1, 2, key="value")

mock.method.assert_called()
mock.method.assert_called_once()
mock.method.assert_called_with(1, 2, key="value")
mock.method.assert_called_once_with(1, 2, key="value")

实战案例:模拟数据库 ​

python
# user_repository.py
class UserRepository:
    def __init__(self, db):
        self.db = db
    
    def get_user(self, user_id):
        return self.db.query(f"SELECT * FROM users WHERE id={user_id}")
    
    def create_user(self, name, email):
        return self.db.execute(f"INSERT INTO users (name, email) VALUES ('{name}', '{email}')")

# test_user_repository.py
from unittest.mock import Mock

def test_get_user():
    # 模拟数据库
    mock_db = Mock()
    mock_db.query.return_value = {"id": 1, "name": "Alice"}
    
    repo = UserRepository(mock_db)
    user = repo.get_user(1)
    
    assert user["name"] == "Alice"
    mock_db.query.assert_called_once_with("SELECT * FROM users WHERE id=1")

def test_create_user():
    mock_db = Mock()
    mock_db.execute.return_value = True
    
    repo = UserRepository(mock_db)
    result = repo.create_user("Bob", "bob@example.com")
    
    assert result is True
    mock_db.execute.assert_called_once()

小结 ​

  • Mock 隔离外部依赖,提高测试速度和可靠性
  • unittest.mock 是标准库
  • pytest-mock 更简洁,与 pytest 集成
  • patch 替换模块对象
  • side_effect 模拟异常和多次调用
  • Mock 外部服务,不 Mock 业务逻辑

下一节学习 调试和性能分析。


参考:unittest.mock 文档

练习:答案

用心记录每一步成长