# Mock测试技术
## 🎯 学习目标
通过本节学习,您将能够:
- 理解Mock测试的概念和重要性
- 掌握Python unittest.mock模块的使用
- 学会为Chat-Room项目编写Mock测试
- 了解测试替身的不同类型和应用场景
- 掌握依赖隔离和测试独立性的实现方法
## 📖 内容概览
Mock测试是单元测试中的重要技术,通过创建模拟对象来替代真实的依赖项,使测试更加独立、快速和可控。本节将详细介绍Mock测试的原理、实现方法,并结合Chat-Room项目的实际场景进行实践。
## 🎭 Mock测试基础概念
### 什么是Mock测试
Mock测试是一种测试技术,通过创建模拟对象(Mock Object)来替代被测试代码的外部依赖,从而实现:
- **隔离测试**:消除外部依赖的影响
- **控制环境**:精确控制测试条件
- **提高速度**:避免耗时的外部调用
- **增强可靠性**:减少测试的不确定性
### 测试替身类型
```mermaid
graph TD
A[测试替身 Test Doubles] --> B[Dummy 哑对象]
A --> C[Stub 存根]
A --> D[Spy 间谍]
A --> E[Mock 模拟对象]
A --> F[Fake 伪对象]
B --> B1[仅用于填充参数
不参与实际逻辑]
C --> C1[返回预设的响应
用于状态验证]
D --> D2[记录调用信息
用于行为验证]
E --> E1[验证交互行为
最严格的验证]
F --> F1[简化的真实实现
用于集成测试]
style A fill:#e8f5e8
style E fill:#fff2cc
```
## 🐍 Python Mock模块详解
### unittest.mock基础用法
```python
import unittest
from unittest.mock import Mock, patch, MagicMock
import socket
import json
# Chat-Room项目中的示例类
class ChatServer:
"""聊天服务器类"""
def __init__(self, host='localhost', port=8888):
self.host = host
self.port = port
self.socket = None
self.clients = {}
def start_server(self):
"""启动服务器"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.bind((self.host, self.port))
self.socket.listen(5)
return True
def send_message(self, client_id, message):
"""发送消息给客户端"""
if client_id in self.clients:
client_socket = self.clients[client_id]
data = json.dumps({'type': 'message', 'content': message})
client_socket.send(data.encode())
return True
return False
def broadcast_message(self, message, sender_id=None):
"""广播消息给所有客户端"""
data = json.dumps({
'type': 'broadcast',
'content': message,
'sender': sender_id
})
success_count = 0
for client_id, client_socket in self.clients.items():
if client_id != sender_id: # 不发送给发送者自己
try:
client_socket.send(data.encode())
success_count += 1
except Exception:
# 发送失败,可能客户端已断开
pass
return success_count
class TestChatServerMock(unittest.TestCase):
"""Chat服务器Mock测试类"""
def setUp(self):
"""测试前准备"""
self.server = ChatServer()
@patch('socket.socket')
def test_start_server_success(self, mock_socket_class):
"""测试服务器启动成功"""
# 创建mock socket实例
mock_socket_instance = Mock()
mock_socket_class.return_value = mock_socket_instance
# 执行测试
result = self.server.start_server()
# 验证结果
self.assertTrue(result)
# 验证socket调用
mock_socket_class.assert_called_once_with(
socket.AF_INET, socket.SOCK_STREAM
)
mock_socket_instance.bind.assert_called_once_with(('localhost', 8888))
mock_socket_instance.listen.assert_called_once_with(5)
def test_send_message_to_existing_client(self):
"""测试向存在的客户端发送消息"""
# 创建mock客户端socket
mock_client_socket = Mock()
self.server.clients['user1'] = mock_client_socket
# 执行测试
result = self.server.send_message('user1', 'Hello World')
# 验证结果
self.assertTrue(result)
# 验证socket调用
expected_data = json.dumps({
'type': 'message',
'content': 'Hello World'
}).encode()
mock_client_socket.send.assert_called_once_with(expected_data)
def test_send_message_to_nonexistent_client(self):
"""测试向不存在的客户端发送消息"""
result = self.server.send_message('nonexistent', 'Hello')
self.assertFalse(result)
def test_broadcast_message(self):
"""测试广播消息"""
# 创建多个mock客户端
mock_client1 = Mock()
mock_client2 = Mock()
mock_client3 = Mock()
self.server.clients = {
'user1': mock_client1,
'user2': mock_client2,
'user3': mock_client3
}
# 执行广播(user1发送消息)
result = self.server.broadcast_message('Hello everyone!', 'user1')
# 验证结果(应该发送给2个客户端,排除发送者)
self.assertEqual(result, 2)
# 验证消息内容
expected_data = json.dumps({
'type': 'broadcast',
'content': 'Hello everyone!',
'sender': 'user1'
}).encode()
# user1不应该收到消息(是发送者)
mock_client1.send.assert_not_called()
# user2和user3应该收到消息
mock_client2.send.assert_called_once_with(expected_data)
mock_client3.send.assert_called_once_with(expected_data)
def test_broadcast_with_failed_clients(self):
"""测试广播时部分客户端发送失败"""
mock_client1 = Mock()
mock_client2 = Mock()
# 设置client2发送时抛出异常
mock_client2.send.side_effect = ConnectionError("Client disconnected")
self.server.clients = {
'user1': mock_client1,
'user2': mock_client2
}
# 执行广播
result = self.server.broadcast_message('Test message')
# 验证结果(只有1个成功)
self.assertEqual(result, 1)
# 验证两个客户端都尝试发送了
self.assertTrue(mock_client1.send.called)
self.assertTrue(mock_client2.send.called)
```
### 高级Mock技术
```python
from unittest.mock import patch, call, ANY
import sqlite3
from datetime import datetime
class UserManager:
"""用户管理类"""
def __init__(self, db_path='chat.db'):
self.db_path = db_path
def create_user(self, username, password, email):
"""创建新用户"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO users (username, password, email, created_at) VALUES (?, ?, ?, ?)",
(username, password, email, datetime.now())
)
conn.commit()
user_id = cursor.lastrowid
return {'success': True, 'user_id': user_id}
except sqlite3.IntegrityError:
return {'success': False, 'error': 'Username already exists'}
finally:
conn.close()
def get_user_by_username(self, username):
"""根据用户名获取用户信息"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
try:
cursor.execute(
"SELECT id, username, email, created_at FROM users WHERE username = ?",
(username,)
)
row = cursor.fetchone()
if row:
return {
'id': row[0],
'username': row[1],
'email': row[2],
'created_at': row[3]
}
return None
finally:
conn.close()
class TestUserManagerMock(unittest.TestCase):
"""用户管理Mock测试类"""
@patch('sqlite3.connect')
def test_create_user_success(self, mock_connect):
"""测试成功创建用户"""
# 设置mock数据库连接
mock_conn = Mock()
mock_cursor = Mock()
mock_connect.return_value = mock_conn
mock_conn.cursor.return_value = mock_cursor
mock_cursor.lastrowid = 123
# 创建用户管理器
user_manager = UserManager()
# 执行测试
result = user_manager.create_user('testuser', 'password123', 'test@example.com')
# 验证结果
self.assertTrue(result['success'])
self.assertEqual(result['user_id'], 123)
# 验证数据库调用
mock_connect.assert_called_once_with('chat.db')
mock_conn.cursor.assert_called_once()
mock_cursor.execute.assert_called_once()
mock_conn.commit.assert_called_once()
mock_conn.close.assert_called_once()
# 验证SQL语句和参数
call_args = mock_cursor.execute.call_args
sql = call_args[0][0]
params = call_args[0][1]
self.assertIn('INSERT INTO users', sql)
self.assertEqual(params[0], 'testuser')
self.assertEqual(params[1], 'password123')
self.assertEqual(params[2], 'test@example.com')
self.assertIsInstance(params[3], datetime)
@patch('sqlite3.connect')
def test_create_user_duplicate_username(self, mock_connect):
"""测试创建重复用户名"""
# 设置mock数据库连接
mock_conn = Mock()
mock_cursor = Mock()
mock_connect.return_value = mock_conn
mock_conn.cursor.return_value = mock_cursor
# 设置执行SQL时抛出完整性错误
mock_cursor.execute.side_effect = sqlite3.IntegrityError("UNIQUE constraint failed")
user_manager = UserManager()
result = user_manager.create_user('existing_user', 'password', 'email@test.com')
# 验证结果
self.assertFalse(result['success'])
self.assertEqual(result['error'], 'Username already exists')
# 验证数据库连接被正确关闭
mock_conn.close.assert_called_once()
@patch('sqlite3.connect')
def test_get_user_by_username_found(self, mock_connect):
"""测试根据用户名查找用户(找到)"""
# 设置mock数据库返回数据
mock_conn = Mock()
mock_cursor = Mock()
mock_connect.return_value = mock_conn
mock_conn.cursor.return_value = mock_cursor
# 模拟数据库返回的行数据
mock_cursor.fetchone.return_value = (
1, 'testuser', 'test@example.com', '2025-01-17 10:00:00'
)
user_manager = UserManager()
result = user_manager.get_user_by_username('testuser')
# 验证结果
self.assertIsNotNone(result)
self.assertEqual(result['id'], 1)
self.assertEqual(result['username'], 'testuser')
self.assertEqual(result['email'], 'test@example.com')
# 验证SQL查询
mock_cursor.execute.assert_called_once_with(
"SELECT id, username, email, created_at FROM users WHERE username = ?",
('testuser',)
)
@patch('sqlite3.connect')
def test_get_user_by_username_not_found(self, mock_connect):
"""测试根据用户名查找用户(未找到)"""
mock_conn = Mock()
mock_cursor = Mock()
mock_connect.return_value = mock_conn
mock_conn.cursor.return_value = mock_cursor
# 模拟数据库未找到数据
mock_cursor.fetchone.return_value = None
user_manager = UserManager()
result = user_manager.get_user_by_username('nonexistent')
# 验证结果
self.assertIsNone(result)
```
## 🔧 Mock测试最佳实践
### 1. Mock的粒度控制
```mermaid
graph TD
A[Mock粒度选择] --> B[过度Mock]
A --> C[适度Mock]
A --> D[不足Mock]
B --> B1[测试变得脆弱
难以维护]
C --> C1[测试独立可靠
易于维护]
D --> D1[测试依赖外部
不够稳定]
style C fill:#e8f5e8
style B fill:#ffcccc
style D fill:#ffcccc
```
### 2. Mock对象的生命周期管理
```python
class TestChatRoomIntegration(unittest.TestCase):
"""聊天室集成测试"""
def setUp(self):
"""每个测试前的准备工作"""
self.mock_database = Mock()
self.mock_network = Mock()
# 创建被测试对象,注入mock依赖
self.chat_room = ChatRoom(
database=self.mock_database,
network=self.mock_network
)
def tearDown(self):
"""每个测试后的清理工作"""
# 重置mock对象状态
self.mock_database.reset_mock()
self.mock_network.reset_mock()
def test_user_join_room(self):
"""测试用户加入聊天室"""
# 设置mock行为
self.mock_database.get_user.return_value = {'id': 1, 'name': 'testuser'}
self.mock_database.add_user_to_room.return_value = True
# 执行测试
result = self.chat_room.join_room('testuser', 'general')
# 验证结果和交互
self.assertTrue(result)
self.mock_database.get_user.assert_called_once_with('testuser')
self.mock_database.add_user_to_room.assert_called_once_with(1, 'general')
```
### 3. 复杂场景的Mock策略
```python
from unittest.mock import patch, PropertyMock
import asyncio
class AIAssistant:
"""AI助手类"""
def __init__(self, api_key, model='glm-4-flash'):
self.api_key = api_key
self.model = model
self.session = None
async def get_response(self, message, context=None):
"""获取AI响应"""
import aiohttp
async with aiohttp.ClientSession() as session:
headers = {'Authorization': f'Bearer {self.api_key}'}
data = {
'model': self.model,
'messages': [{'role': 'user', 'content': message}]
}
if context:
data['messages'].insert(0, {'role': 'system', 'content': context})
async with session.post(
'https://api.example.com/chat',
headers=headers,
json=data
) as response:
if response.status == 200:
result = await response.json()
return result['choices'][0]['message']['content']
else:
raise Exception(f"API Error: {response.status}")
class TestAIAssistantMock(unittest.TestCase):
"""AI助手Mock测试"""
@patch('aiohttp.ClientSession')
async def test_get_response_success(self, mock_session_class):
"""测试成功获取AI响应"""
# 创建mock响应对象
mock_response = Mock()
mock_response.status = 200
mock_response.json = Mock(return_value=asyncio.coroutine(lambda: {
'choices': [{'message': {'content': 'Hello! How can I help you?'}}]
})())
# 创建mock session
mock_session = Mock()
mock_session.post.return_value.__aenter__.return_value = mock_response
mock_session_class.return_value.__aenter__.return_value = mock_session
# 执行测试
ai_assistant = AIAssistant('test-api-key')
result = await ai_assistant.get_response('Hello')
# 验证结果
self.assertEqual(result, 'Hello! How can I help you?')
# 验证API调用
mock_session.post.assert_called_once()
call_args = mock_session.post.call_args
self.assertEqual(call_args[0][0], 'https://api.example.com/chat')
self.assertIn('Authorization', call_args[1]['headers'])
@patch('aiohttp.ClientSession')
async def test_get_response_api_error(self, mock_session_class):
"""测试API错误响应"""
# 设置mock返回错误状态
mock_response = Mock()
mock_response.status = 500
mock_session = Mock()
mock_session.post.return_value.__aenter__.return_value = mock_response
mock_session_class.return_value.__aenter__.return_value = mock_session
ai_assistant = AIAssistant('test-api-key')
# 验证异常抛出
with self.assertRaises(Exception) as context:
await ai_assistant.get_response('Hello')
self.assertIn('API Error: 500', str(context.exception))
```
## 📊 Mock测试的度量和评估
### 测试覆盖率分析
```python
# 运行测试并生成覆盖率报告
# pip install coverage
# coverage run -m pytest tests/
# coverage report -m
# coverage html
class TestCoverageExample(unittest.TestCase):
"""测试覆盖率示例"""
@patch('requests.get')
def test_api_call_with_different_responses(self, mock_get):
"""测试不同API响应的处理"""
# 测试成功响应
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'status': 'success'}
result = api_call('test-endpoint')
self.assertEqual(result['status'], 'success')
# 测试错误响应
mock_get.return_value.status_code = 404
result = api_call('test-endpoint')
self.assertIsNone(result)
# 测试网络异常
mock_get.side_effect = requests.ConnectionError()
result = api_call('test-endpoint')
self.assertIsNone(result)
```
## 🎯 Chat-Room项目Mock测试实践
### 完整的测试套件示例
```python
# tests/test_chat_server_mock.py
import unittest
from unittest.mock import Mock, patch, call
import sys
import os
# 添加项目根目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from server.chat_server import ChatServer
from shared.message_protocol import MessageProtocol
class TestChatServerComplete(unittest.TestCase):
"""完整的聊天服务器Mock测试套件"""
def setUp(self):
"""测试准备"""
self.server = ChatServer()
self.mock_clients = {}
# 创建多个mock客户端
for i in range(3):
self.mock_clients[f'user{i+1}'] = Mock()
@patch('server.chat_server.socket.socket')
@patch('server.chat_server.threading.Thread')
def test_server_lifecycle(self, mock_thread, mock_socket):
"""测试服务器完整生命周期"""
# 启动服务器
self.server.start()
# 验证socket创建和绑定
mock_socket.assert_called()
# 验证线程启动
mock_thread.assert_called()
# 停止服务器
self.server.stop()
# 验证清理工作
self.assertFalse(self.server.running)
def test_message_routing_scenarios(self):
"""测试各种消息路由场景"""
self.server.clients = self.mock_clients.copy()
# 场景1:私聊消息
self.server.route_private_message('user1', 'user2', 'Hello user2!')
# 验证只有user2收到消息
self.mock_clients['user2'].send.assert_called_once()
self.mock_clients['user1'].send.assert_not_called()
self.mock_clients['user3'].send.assert_not_called()
# 重置mock
for client in self.mock_clients.values():
client.reset_mock()
# 场景2:群组广播
self.server.broadcast_to_group('general', 'user1', 'Hello everyone!')
# 验证除发送者外的所有用户都收到消息
self.mock_clients['user1'].send.assert_not_called()
self.mock_clients['user2'].send.assert_called_once()
self.mock_clients['user3'].send.assert_called_once()
if __name__ == '__main__':
unittest.main()
```
## 📚 学习总结
Mock测试是现代软件测试的重要技术,通过本节学习,您应该掌握:
1. **Mock概念**:理解测试替身的类型和用途
2. **Python Mock**:熟练使用unittest.mock模块
3. **最佳实践**:掌握Mock测试的设计原则
4. **实际应用**:能够为Chat-Room项目编写Mock测试
## 🎯 实践练习
1. 为Chat-Room的文件传输功能编写Mock测试
2. 创建AI助手模块的异步Mock测试
3. 实现数据库操作的完整Mock测试套件
4. 设计网络通信的Mock测试场景
---
**Mock测试让单元测试更加独立、快速和可靠!** 🎭
## 📖 导航
⬅️ **上一节:** [Test Coverage](test-coverage.md)
📚 **返回:** [第15章:测试开发](README.md)
🏠 **主页:** [学习路径总览](../README.md)
*本节最后更新:2025-01-17*