# 测试策略与实践学习 ## 📋 模块概述 测试是软件开发的重要环节,Chat-Room项目采用了多层次的测试策略,包括单元测试、集成测试、功能测试和性能测试。本文档详细介绍现代Python项目的测试最佳实践。 ## 🎯 测试金字塔 ### 测试层次结构 ```mermaid graph TD A[测试金字塔] --> B[单元测试
Unit Tests] A --> C[集成测试
Integration Tests] A --> D[端到端测试
E2E Tests] B --> B1[数量最多
运行最快] B --> B2[测试单个函数/类] B --> B3[Mock外部依赖] C --> C1[数量中等
运行较快] C --> C2[测试模块间交互] C --> C3[真实依赖] D --> D1[数量最少
运行最慢] D --> D2[测试完整流程] D --> D3[真实环境] style B fill:#e8f5e8 style C fill:#fff3cd style D fill:#f8d7da ``` **测试比例建议**: - **单元测试**:70% - 快速反馈,高覆盖率 - **集成测试**:20% - 验证模块协作 - **端到端测试**:10% - 验证用户场景 ## 🧪 单元测试实践 ### pytest基础配置 ```python # conftest.py - pytest配置文件 import pytest import tempfile import os from unittest.mock import Mock, patch from server.database.models import DatabaseManager from shared.logger import get_logger @pytest.fixture(scope="session") def test_database(): """测试数据库fixture""" # 创建临时数据库文件 with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp_file: test_db_path = tmp_file.name # 初始化测试数据库 db = DatabaseManager(test_db_path) db.init_database() yield db # 清理 os.unlink(test_db_path) @pytest.fixture def mock_logger(): """Mock日志器fixture""" with patch('shared.logger.get_logger') as mock_get_logger: mock_logger = Mock() mock_get_logger.return_value = mock_logger yield mock_logger @pytest.fixture def sample_user_data(): """示例用户数据fixture""" return { 'username': 'test_user', 'password': 'test_password123', 'user_id': 1 } @pytest.fixture def sample_chat_message(): """示例聊天消息fixture""" from shared.messages import ChatMessage return ChatMessage( sender_id=1, sender_username='test_user', chat_group_id=1, chat_group_name='test_group', content='Hello, World!', timestamp=1234567890.0 ) ``` ### 数据库操作单元测试 ```python # test/test_database.py import pytest from server.database.models import DatabaseManager from shared.exceptions import UserAlreadyExistsError, UserNotFoundError class TestUserOperations: """用户操作测试类""" def test_create_user_success(self, test_database, sample_user_data): """测试成功创建用户""" db = test_database # 执行创建用户 user_id = db.create_user( sample_user_data['username'], sample_user_data['password'] ) # 验证结果 assert user_id is not None assert isinstance(user_id, int) assert user_id > 0 # 验证用户确实被创建 user_info = db.get_user_by_id(user_id) assert user_info['username'] == sample_user_data['username'] assert user_info['is_online'] is False assert user_info['is_banned'] is False def test_create_user_duplicate_username(self, test_database, sample_user_data): """测试创建重复用户名的用户""" db = test_database # 先创建一个用户 db.create_user(sample_user_data['username'], sample_user_data['password']) # 尝试创建同名用户,应该抛出异常 with pytest.raises(UserAlreadyExistsError): db.create_user(sample_user_data['username'], 'different_password') def test_create_user_invalid_input(self, test_database): """测试无效输入创建用户""" db = test_database # 测试空用户名 with pytest.raises(ValueError): db.create_user('', 'password123') # 测试空密码 with pytest.raises(ValueError): db.create_user('testuser', '') # 测试用户名过短 with pytest.raises(ValueError): db.create_user('ab', 'password123') # 测试密码过短 with pytest.raises(ValueError): db.create_user('testuser', '123') def test_authenticate_user_success(self, test_database, sample_user_data): """测试成功认证用户""" db = test_database # 创建用户 user_id = db.create_user( sample_user_data['username'], sample_user_data['password'] ) # 认证用户 auth_result = db.authenticate_user( sample_user_data['username'], sample_user_data['password'] ) # 验证认证结果 assert auth_result is not None assert auth_result['id'] == user_id assert auth_result['username'] == sample_user_data['username'] def test_authenticate_user_wrong_password(self, test_database, sample_user_data): """测试错误密码认证""" db = test_database # 创建用户 db.create_user(sample_user_data['username'], sample_user_data['password']) # 使用错误密码认证 auth_result = db.authenticate_user( sample_user_data['username'], 'wrong_password' ) # 应该返回None assert auth_result is None def test_get_user_by_id_not_found(self, test_database): """测试获取不存在的用户""" db = test_database with pytest.raises(UserNotFoundError): db.get_user_by_id(99999) @pytest.mark.parametrize("is_online,expected", [ (True, True), (False, False), ]) def test_update_user_status(self, test_database, sample_user_data, is_online, expected): """测试更新用户状态(参数化测试)""" db = test_database # 创建用户 user_id = db.create_user( sample_user_data['username'], sample_user_data['password'] ) # 更新状态 db.update_user_status(user_id, is_online) # 验证状态 user_info = db.get_user_by_id(user_id) assert user_info['is_online'] == expected ``` ### 消息处理单元测试 ```python # test/test_message_processing.py import pytest from unittest.mock import Mock, patch, MagicMock from server.core.chat_manager import ChatManager from shared.messages import ChatMessage from shared.exceptions import PermissionDeniedError class TestChatManager: """聊天管理器测试类""" @pytest.fixture def mock_user_manager(self): """Mock用户管理器""" mock_manager = Mock() mock_manager.is_user_online.return_value = True mock_manager.get_user_socket.return_value = Mock() return mock_manager @pytest.fixture def chat_manager(self, mock_user_manager): """聊天管理器实例""" with patch('server.core.chat_manager.get_db') as mock_get_db: mock_db = Mock() mock_get_db.return_value = mock_db manager = ChatManager(mock_user_manager) manager.db = mock_db return manager def test_send_message_success(self, chat_manager, sample_chat_message): """测试成功发送消息""" # 设置Mock返回值 chat_manager.db.get_user_by_id.return_value = {'username': 'test_user'} chat_manager.db.get_chat_group_by_id.return_value = {'name': 'test_group'} chat_manager.db.save_message.return_value = 123 chat_manager.db.is_user_in_chat_group.return_value = True # 执行发送消息 result = chat_manager.send_message( sender_id=1, group_id=1, content="Hello, World!" ) # 验证结果 assert result is not None assert result.message_id == 123 assert result.content == "Hello, World!" # 验证数据库调用 chat_manager.db.save_message.assert_called_once() def test_send_message_permission_denied(self, chat_manager): """测试无权限发送消息""" # 设置用户不在聊天组中 chat_manager.db.is_user_in_chat_group.return_value = False # 尝试发送消息,应该抛出权限异常 with pytest.raises(PermissionDeniedError): chat_manager.send_message( sender_id=1, group_id=1, content="Hello, World!" ) def test_broadcast_message_to_group(self, chat_manager, sample_chat_message): """测试消息广播""" # 设置Mock数据 mock_members = [ {'id': 1, 'username': 'user1'}, {'id': 2, 'username': 'user2'}, {'id': 3, 'username': 'user3'} ] chat_manager.db.get_chat_group_members.return_value = mock_members # 设置用户管理器Mock chat_manager.user_manager.is_user_online.side_effect = [True, False, True] chat_manager.user_manager.get_user_current_chat.return_value = 1 mock_socket1 = Mock() mock_socket3 = Mock() chat_manager.user_manager.get_user_socket.side_effect = [mock_socket1, None, mock_socket3] # 执行广播 chat_manager.broadcast_message_to_group(sample_chat_message) # 验证Socket发送调用 mock_socket1.send.assert_called_once() mock_socket3.send.assert_called_once() ``` ## 🔗 集成测试实践 ### 服务器-数据库集成测试 ```python # test/test_integration.py import pytest import threading import time import socket from server.core.server import ChatRoomServer from client.core.client import NetworkClient from shared.messages import LoginRequest, ChatMessage class TestServerIntegration: """服务器集成测试""" @pytest.fixture(scope="class") def test_server(self, test_database): """测试服务器fixture""" server = ChatRoomServer(host='localhost', port=0) # 使用随机端口 server.db = test_database # 在后台线程启动服务器 server_thread = threading.Thread(target=server.start, daemon=True) server_thread.start() # 等待服务器启动 time.sleep(0.5) # 获取实际端口 actual_port = server.server_socket.getsockname()[1] server.actual_port = actual_port yield server # 清理 server.stop() def test_client_server_communication(self, test_server, sample_user_data): """测试客户端-服务器通信""" # 创建测试用户 test_server.db.create_user( sample_user_data['username'], sample_user_data['password'] ) # 创建客户端 client = NetworkClient('localhost', test_server.actual_port) try: # 连接服务器 assert client.connect() is True # 发送登录请求 login_request = LoginRequest( username=sample_user_data['username'], password=sample_user_data['password'] ) response_received = threading.Event() login_response = None def handle_login_response(message): nonlocal login_response login_response = message response_received.set() client.register_message_handler('login_response', handle_login_response) # 发送登录请求 client.send_message(login_request) # 等待响应 assert response_received.wait(timeout=5.0) assert login_response is not None assert login_response.success is True finally: client.disconnect() def test_multiple_clients_chat(self, test_server): """测试多客户端聊天""" # 创建测试用户 user1_data = {'username': 'user1', 'password': 'pass1'} user2_data = {'username': 'user2', 'password': 'pass2'} test_server.db.create_user(user1_data['username'], user1_data['password']) test_server.db.create_user(user2_data['username'], user2_data['password']) # 创建两个客户端 client1 = NetworkClient('localhost', test_server.actual_port) client2 = NetworkClient('localhost', test_server.actual_port) try: # 连接并登录 assert client1.connect() is True assert client2.connect() is True # 登录用户1 login1 = LoginRequest(**user1_data) client1.send_message(login1) # 登录用户2 login2 = LoginRequest(**user2_data) client2.send_message(login2) time.sleep(0.5) # 等待登录完成 # 用户1发送消息 chat_msg = ChatMessage( content="Hello from user1", chat_group_id=1 # 默认公共聊天组 ) message_received = threading.Event() received_message = None def handle_chat_message(message): nonlocal received_message received_message = message message_received.set() client2.register_message_handler('chat_message', handle_chat_message) # 发送消息 client1.send_message(chat_msg) # 验证用户2收到消息 assert message_received.wait(timeout=5.0) assert received_message is not None assert received_message.content == "Hello from user1" finally: client1.disconnect() client2.disconnect() ``` ## 🎭 Mock和Stub技术 ### AI服务Mock ```python # test/test_ai_integration.py import pytest from unittest.mock import Mock, patch, MagicMock from server.ai.ai_manager import AIManager class TestAIIntegration: """AI集成测试""" @pytest.fixture def mock_zhipu_client(self): """Mock智谱AI客户端""" mock_client = Mock() # 模拟成功响应 mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message.content = "这是AI的回复" mock_response.usage.prompt_tokens = 10 mock_response.usage.completion_tokens = 5 mock_response.usage.total_tokens = 15 mock_client.chat.completions.create.return_value = mock_response return mock_client @patch('server.ai.ai_manager.ZhipuAI') def test_ai_manager_initialization(self, mock_zhipu_class, mock_zhipu_client): """测试AI管理器初始化""" mock_zhipu_class.return_value = mock_zhipu_client mock_zhipu_client.chat.completions.create.return_value = Mock() # 创建AI管理器 ai_manager = AIManager(api_key="test_key") # 验证初始化 assert ai_manager.enabled is True mock_zhipu_class.assert_called_once_with(api_key="test_key") def test_process_message_success(self, mock_zhipu_client): """测试成功处理AI消息""" with patch('server.ai.ai_manager.ZhipuAI', return_value=mock_zhipu_client): ai_manager = AIManager(api_key="test_key") # 处理消息 response = ai_manager.process_message( user_id=1, username="test_user", message_content="你好", chat_group_id=1 ) # 验证响应 assert response == "这是AI的回复" # 验证API调用 mock_zhipu_client.chat.completions.create.assert_called_once() def test_process_message_api_error(self, mock_zhipu_client): """测试AI API错误处理""" # 模拟API错误 mock_zhipu_client.chat.completions.create.side_effect = Exception("API Error") with patch('server.ai.ai_manager.ZhipuAI', return_value=mock_zhipu_client): ai_manager = AIManager(api_key="test_key") # 处理消息 response = ai_manager.process_message( user_id=1, username="test_user", message_content="你好", chat_group_id=1 ) # 应该返回错误消息 assert "无法回复" in response ``` ## 📊 性能测试 ### 并发连接测试 ```python # test/test_performance.py import pytest import threading import time import statistics from concurrent.futures import ThreadPoolExecutor, as_completed from client.core.client import NetworkClient class TestPerformance: """性能测试类""" @pytest.mark.performance def test_concurrent_connections(self, test_server): """测试并发连接性能""" num_clients = 50 connection_times = [] def create_connection(): """创建单个连接并测量时间""" start_time = time.time() client = NetworkClient('localhost', test_server.actual_port) try: success = client.connect() end_time = time.time() if success: connection_times.append(end_time - start_time) return True return False finally: client.disconnect() # 并发创建连接 with ThreadPoolExecutor(max_workers=num_clients) as executor: futures = [executor.submit(create_connection) for _ in range(num_clients)] successful_connections = 0 for future in as_completed(futures): if future.result(): successful_connections += 1 # 验证性能指标 assert successful_connections >= num_clients * 0.9 # 至少90%成功 if connection_times: avg_time = statistics.mean(connection_times) max_time = max(connection_times) print(f"平均连接时间: {avg_time:.3f}s") print(f"最大连接时间: {max_time:.3f}s") print(f"成功连接数: {successful_connections}/{num_clients}") # 性能断言 assert avg_time < 1.0 # 平均连接时间小于1秒 assert max_time < 5.0 # 最大连接时间小于5秒 @pytest.mark.performance def test_message_throughput(self, test_server): """测试消息吞吐量""" num_messages = 1000 num_clients = 10 # 创建测试用户 for i in range(num_clients): test_server.db.create_user(f'user{i}', f'pass{i}') def send_messages(client_id): """单个客户端发送消息""" client = NetworkClient('localhost', test_server.actual_port) try: client.connect() # 登录 login_request = LoginRequest( username=f'user{client_id}', password=f'pass{client_id}' ) client.send_message(login_request) time.sleep(0.1) # 等待登录完成 # 发送消息 start_time = time.time() for i in range(num_messages // num_clients): chat_msg = ChatMessage( content=f"Message {i} from client {client_id}", chat_group_id=1 ) client.send_message(chat_msg) end_time = time.time() return end_time - start_time finally: client.disconnect() # 并发发送消息 with ThreadPoolExecutor(max_workers=num_clients) as executor: futures = [executor.submit(send_messages, i) for i in range(num_clients)] total_time = max(future.result() for future in as_completed(futures)) # 计算吞吐量 throughput = num_messages / total_time print(f"消息吞吐量: {throughput:.2f} 消息/秒") # 性能断言 assert throughput > 100 # 至少100消息/秒 ``` ## 🎯 测试覆盖率 ### 覆盖率配置 ```python # pytest.ini [tool:pytest] testpaths = test python_files = test_*.py python_classes = Test* python_functions = test_* addopts = --cov=server --cov=client --cov=shared --cov-report=html --cov-report=term-missing --cov-fail-under=80 markers = unit: 单元测试 integration: 集成测试 performance: 性能测试 slow: 慢速测试 ``` ### 覆盖率分析 ```bash # 运行测试并生成覆盖率报告 pytest --cov=server --cov=client --cov=shared --cov-report=html # 只运行单元测试 pytest -m unit # 排除慢速测试 pytest -m "not slow" # 运行特定模块测试 pytest test/test_database.py -v ``` ## 💡 学习要点 ### 测试设计原则 1. **FIRST原则**:Fast(快速)、Independent(独立)、Repeatable(可重复)、Self-Validating(自验证)、Timely(及时) 2. **AAA模式**:Arrange(准备)、Act(执行)、Assert(断言) 3. **单一职责**:每个测试只验证一个功能点 4. **可读性**:测试代码要清晰易懂 ### Mock使用技巧 1. **隔离依赖**:Mock外部依赖,专注测试目标 2. **行为验证**:验证方法调用和参数 3. **状态验证**:验证对象状态变化 4. **边界测试**:测试异常情况和边界条件 ### 测试策略 1. **测试驱动开发(TDD)**:先写测试,再写实现 2. **行为驱动开发(BDD)**:从用户行为角度设计测试 3. **持续集成**:自动化测试执行 4. **测试左移**:在开发早期引入测试 ## 🤔 思考题 1. **如何平衡测试覆盖率和开发效率?** - 关注核心业务逻辑的测试 - 使用风险驱动的测试策略 - 自动化重复性测试 2. **如何测试异步和并发代码?** - 使用pytest-asyncio插件 - Mock时间相关的操作 - 控制并发执行顺序 3. **如何维护测试代码的质量?** - 重构测试代码 - 共享测试工具和fixture - 定期审查测试用例 --- **下一步**:学习TUI界面高级特性 → [ui-advanced.md](./ui-advanced.md)