一、测试体系概述
1.1 为什么需要测试
┌─────────────────────────────────────────────────────────────────┐
│ 测试的价值 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 质量保障 │───>│ 信心提升 │───>│ 持续交付 │ │
│ │ │ │ │ │ │ │
│ │ 发现问题 │ │ 发布前发现 │ │ 快速迭代 │ │
│ │ 预防缺陷 │ │ 降低线上风险 │ │ 保障进度 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 没有测试的后果: │
│ ├── 线上故障频发,用户体验差 │
│ ├── 每次发版提心吊胆 │
│ ├── 问题定位困难,修复成本高 │
│ └── 团队信誉受损,客户流失 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 测试金字塔
scss
┌─────────────────────────────────────────────────────────────────┐
│ 测试金字塔 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ▲ │
│ /█\ │
│ / █ \ │
│ / █ \ E2E测试 │
│ / █ \ (少量、高价值) │
│ /────█────\ │
│ / █ \ 集成测试 │
│ / █ \ (接口、模块) │
│ /───────█───────\ │
│ / █ \ 单元测试 │
│ /─────────█─────────\ (大量、快速) │
│ │
│ 比例: 单元测试 70% | 集成测试 20% | E2E测试 10% │
│ │
└─────────────────────────────────────────────────────────────────┘
二、单元测试
2.1 单元测试框架
python
import pytest
from unittest.mock import Mock, AsyncMock, patch
from datetime import datetime
class TestWeChatClient:
"""微信客户端单元测试"""
@pytest.fixture
def wechat_client(self):
"""创建测试客户端"""
from wechat_bot import WeChatClient
return WeChatClient(app_id="test_app", app_secret="test_secret")
@pytest.fixture
def mock_response(self):
"""模拟API响应"""
return {
"code": 0,
"message": "success",
"data": {
"user_id": "user_123",
"nickname": "测试用户"
}
}
def test_client_initialization(self):
"""测试客户端初始化"""
client = WeChatClient(app_id="test", app_secret="secret")
assert client.app_id == "test"
assert client.app_secret == "secret"
assert client.is_connected is False
def test_send_text_message_success(self, wechat_client, mock_response):
"""测试发送文本消息成功"""
with patch('requests.post', return_value=Mock(json=lambda: mock_response)):
result = wechat_client.send_text_message("user_123", "你好")
assert result["code"] == 0
assert wechat_client.is_connected is True
def test_send_text_message_empty_content(self, wechat_client):
"""测试发送空消息"""
with pytest.raises(ValueError, match="消息内容不能为空"):
wechat_client.send_text_message("user_123", "")
def test_send_text_message_too_long(self, wechat_client):
"""测试消息内容过长"""
long_content = "a" * 10001
with pytest.raises(ValueError, match="消息内容过长"):
wechat_client.send_text_message("user_123", long_content)
def test_get_user_info(self, wechat_client, mock_response):
"""测试获取用户信息"""
with patch('requests.get', return_value=Mock(json=lambda: mock_response)):
user_info = wechat_client.get_user_info("user_123")
assert user_info["user_id"] == "user_123"
assert user_info["nickname"] == "测试用户"
class TestMessageHandler:
"""消息处理器单元测试"""
@pytest.fixture
def handler(self):
"""创建消息处理器"""
from message_handler import MessageHandler
return MessageHandler()
@pytest.mark.asyncio
async def test_handle_text_message(self, handler):
"""测试处理文本消息"""
message = {
"msg_type": "text",
"content": "测试消息",
"from_user": "user_123"
}
response = await handler.handle_message(message)
assert response is not None
assert "content" in response
@pytest.mark.asyncio
async def test_handle_keyword_reply(self, handler):
"""测试关键词回复"""
handler.add_keyword_rule("你好", "你好!有什么可以帮你的吗?")
message = {
"msg_type": "text",
"content": "你好",
"from_user": "user_123"
}
response = await handler.handle_message(message)
assert "你好" in response["content"]
@pytest.mark.asyncio
async def test_no_match_keyword(self, handler):
"""测试无匹配关键词"""
message = {
"msg_type": "text",
"content": "未配置的关键词",
"from_user": "user_123"
}
response = await handler.handle_message(message)
assert response is None
2.2 测试数据管理
python
import pytest
from dataclasses import dataclass
from typing import List
@dataclass
class TestUser:
"""测试用户数据"""
user_id: str
nickname: str
is_vip: bool = False
tags: List[str] = None
def __post_init__(self):
if self.tags is None:
self.tags = []
class TestDataFactory:
"""测试数据工厂"""
@staticmethod
def create_test_user(user_id: str = "test_user_001", **kwargs) -> TestUser:
"""创建测试用户"""
defaults = {
"user_id": user_id,
"nickname": f"测试用户_{user_id}",
"is_vip": False,
"tags": ["测试用户"]
}
defaults.update(kwargs)
return TestUser(**defaults)
@staticmethod
def create_test_message(
msg_type: str = "text",
content: str = "测试消息",
from_user: str = "test_user_001",
**kwargs
) -> dict:
"""创建测试消息"""
message = {
"msg_type": msg_type,
"content": content,
"from_user": from_user,
"msg_id": f"msg_{datetime.now().timestamp()}",
"create_time": datetime.now().timestamp()
}
message.update(kwargs)
return message
@staticmethod
def create_batch_users(count: int) -> List[TestUser]:
"""批量创建测试用户"""
return [
TestDataFactory.create_test_user(f"test_user_{i:03d}")
for i in range(1, count + 1)
]
@pytest.fixture
def test_users():
"""测试用户fixture"""
return TestDataFactory.create_batch_users(10)
@pytest.fixture
def test_messages():
"""测试消息fixture"""
return [
TestDataFactory.create_test_message(content=f"测试消息_{i}")
for i in range(5)
]
三、集成测试
3.1 API集成测试
python
import pytest
from httpx import AsyncClient
class TestWeChatAPI:
"""微信API集成测试"""
@pytest.fixture
async def api_client(self):
"""创建API测试客户端"""
async with AsyncClient(base_url="http://localhost:8000") as client:
yield client
@pytest.fixture
async def authenticated_client(self, api_client):
"""创建已认证的客户端"""
response = await api_client.post("/api/auth/login", json={
"app_id": "test_app",
"app_secret": "test_secret"
})
token = response.json()["access_token"]
api_client.headers["Authorization"] = f"Bearer {token}"
return api_client
@pytest.mark.asyncio
async def test_health_check(self, api_client):
"""测试健康检查接口"""
response = await api_client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"
@pytest.mark.asyncio
async def test_send_message_unauthorized(self, api_client):
"""测试未授权发送消息"""
response = await api_client.post("/api/message/send", json={
"to_user": "user_123",
"content": "测试消息"
})
assert response.status_code == 401
@pytest.mark.asyncio
async def test_send_message_authenticated(self, authenticated_client):
"""测试认证后发送消息"""
response = await authenticated_client.post("/api/message/send", json={
"to_user": "user_123",
"content": "集成测试消息"
})
assert response.status_code == 200
assert response.json()["code"] == 0
@pytest.mark.asyncio
async def test_batch_send_messages(self, authenticated_client):
"""测试批量发送消息"""
messages = [
{"to_user": f"user_{i}", "content": f"批量消息_{i}"}
for i in range(10)
]
response = await authenticated_client.post("/api/message/batch_send", json={
"messages": messages
})
assert response.status_code == 200
assert response.json()["success_count"] == 10
@pytest.mark.asyncio
async def test_get_user_info(self, authenticated_client):
"""测试获取用户信息"""
response = await authenticated_client.get("/api/user/user_123")
assert response.status_code == 200
data = response.json()
assert "user_id" in data
assert "nickname" in data
@pytest.mark.asyncio
async def test_rate_limit(self, authenticated_client):
"""测试限流"""
for i in range(105):
response = await authenticated_client.get("/api/message/list")
assert response.status_code == 429
class TestDatabaseIntegration:
"""数据库集成测试"""
@pytest.fixture
async def db_client(self):
"""创建数据库客户端"""
from db import DatabaseClient
client = DatabaseClient("sqlite:///test.db")
await client.initialize()
yield client
await client.close()
@pytest.mark.asyncio
async def test_save_and_retrieve_message(self, db_client):
"""测试保存和检索消息"""
message_data = {
"msg_id": "msg_001",
"user_id": "user_001",
"content": "测试消息",
"msg_type": "text"
}
await db_client.save_message(message_data)
retrieved = await db_client.get_message("msg_001")
assert retrieved["msg_id"] == "msg_001"
assert retrieved["content"] == "测试消息"
@pytest.mark.asyncio
async def test_message_history(self, db_client):
"""测试消息历史查询"""
user_id = "user_001"
for i in range(5):
await db_client.save_message({
"msg_id": f"msg_{i}",
"user_id": user_id,
"content": f"消息_{i}"
})
history = await db_client.get_user_message_history(user_id, limit=10)
assert len(history) == 5
四、端到端测试
4.1 E2E测试框架
python
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestWeChatBotE2E:
"""微信机器人端到端测试"""
@pytest.fixture(scope="class")
def driver(self):
"""创建浏览器驱动"""
options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)
yield driver
driver.quit()
@pytest.fixture
def logged_in_driver(self, driver):
"""登录后的驱动"""
driver.get("http://localhost:3000/login")
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.ID, "username"))).send_keys("admin")
driver.find_element(By.ID, "password").send_keys("admin123")
driver.find_element(By.ID, "login-btn").click()
wait.until(EC.url_to_be("http://localhost:3000/dashboard"))
return driver
def test_user_can_view_dashboard(self, logged_in_driver):
"""测试用户可以查看仪表盘"""
driver = logged_in_driver
assert "仪表盘" in driver.page_source or "Dashboard" in driver.page_source
stats_cards = driver.find_elements(By.CLASS_NAME, "stat-card")
assert len(stats_cards) > 0
def test_user_can_send_test_message(self, logged_in_driver):
"""测试发送测试消息"""
driver = logged_in_driver
driver.get("http://localhost:3000/message/send")
user_input = driver.find_element(By.ID, "user-input")
user_input.send_keys("user_001")
content_input = driver.find_element(By.ID, "message-content")
content_input.send_keys("这是一条E2E测试消息")
send_btn = driver.find_element(By.ID, "send-btn")
send_btn.click()
toast = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.CLASS_NAME, "toast"))
)
assert "success" in toast.text.lower() or "成功" in toast.text
class TestWeChatWorkflow:
"""微信工作流E2E测试"""
@pytest.fixture
def mock_wechat_server(self):
"""模拟微信服务器"""
from unittest.mock import Mock, patch
mock_server = Mock()
mock_server.send_message.return_value = {"code": 0, "message": "success"}
return mock_server
def test_complete_user_flow(self, mock_wechat_server):
"""测试完整用户流程"""
with patch('wechat_bot.server', mock_wechat_server):
user_id = "test_user_flow"
from wechat_bot import WeChatBot
bot = WeChatBot()
bot.handle_join(user_id)
assert mock_wechat_server.send_message.called
welcome_call = mock_wechat_server.send_message.call_args_list[0]
assert "欢迎" in str(welcome_call)
五、自动化测试流水线
5.1 CI/CD测试配置
yaml
# .github/workflows/test.yml
name: Test Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install pytest pytest-asyncio pytest-cov
pip install -r requirements.txt
- name: Run unit tests
run: |
pytest tests/unit \
--cov=src \
--cov-report=xml \
--cov-report=html
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
integration-test:
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Run integration tests
run: |
pytest tests/integration -v
e2e-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Build
run: npm ci && npm run build
- name: Run E2E tests
run: |
npx playwright install --with-deps
npx playwright test
5.2 测试报告生成
python
import pytest
import json
from datetime import datetime
from typing import Dict, List
class TestReportGenerator:
"""测试报告生成器"""
def __init__(self):
self.test_results: List[dict] = []
def record_test(self, test_name: str, status: str, duration: float, error: str = None):
"""记录测试结果"""
self.test_results.append({
"test_name": test_name,
"status": status,
"duration": duration,
"error": error,
"timestamp": datetime.now().isoformat()
})
def generate_html_report(self) -> str:
"""生成HTML报告"""
passed = sum(1 for r in self.test_results if r["status"] == "passed")
failed = sum(1 for r in self.test_results if r["status"] == "failed")
skipped = sum(1 for r in self.test_results if r["status"] == "skipped")
total = len(self.test_results)
total_duration = sum(r["duration"] for r in self.test_results)
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>Test Report - {datetime.now().strftime('%Y-%m-%d %H:%M')}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.summary {{ background: #f5f5f5; padding: 20px; border-radius: 8px; }}
.passed {{ color: green; }}
.failed {{ color: red; }}
.skipped {{ color: orange; }}
table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
th {{ background-color: #4CAF50; color: white; }}
</style>
</head>
<body>
<h1>微信机器人测试报告</h1>
<div class="summary">
<h2>测试摘要</h2>
<p>总测试数: <strong>{total}</strong></p>
<p class="passed">通过: {passed}</p>
<p class="failed">失败: {failed}</p>
<p class="skipped">跳过: {skipped}</p>
<p>总耗时: {total_duration:.2f}秒</p>
<p>通过率: {(passed/max(total,1))*100:.1f}%</p>
</div>
<table>
<tr>
<th>测试名称</th>
<th>状态</th>
<th>耗时</th>
<th>错误信息</th>
</tr>
"""
for result in self.test_results:
status_class = result["status"]
error_info = result.get("error", "") or ""
html += f"""
<tr>
<td>{result['test_name']}</td>
<td class="{status_class}">{result['status']}</td>
<td>{result['duration']:.3f}s</td>
<td>{error_info}</td>
</tr>
"""
html += """
</table>
</body>
</html>
"""
return html
六、性能测试
6.1 压力测试
python
import pytest
import asyncio
import time
from locust import HttpUser, task, between
class WeChatBotLoadTest(HttpUser):
"""Locust负载测试"""
wait_time = between(0.1, 0.5)
def on_start(self):
"""初始化"""
response = self.client.post("/api/auth/login", json={
"app_id": "test_app",
"app_secret": "test_secret"
})
self.token = response.json().get("access_token")
self.headers = {"Authorization": f"Bearer {self.token}"}
@task(10)
def send_message(self):
"""发送消息"""
self.client.post("/api/message/send",
headers=self.headers,
json={
"to_user": "user_001",
"content": "负载测试消息"
}
)
@task(5)
def get_user_info(self):
"""获取用户信息"""
self.client.get("/api/user/user_001", headers=self.headers)
@task(3)
def get_message_list(self):
"""获取消息列表"""
self.client.get("/api/message/list", headers=self.headers)
@pytest.fixture
async def stress_test_client():
"""压力测试客户端"""
import aiohttp
async with aiohttp.ClientSession() as session:
yield session
async def test_concurrent_messages(stress_test_client):
"""测试并发消息发送"""
from aiohttp import ClientError
url = "http://localhost:8000/api/message/send"
headers = {"Authorization": "Bearer test_token"}
async def send_message(i):
try:
async with stress_test_client.post(url, headers=headers, json={
"to_user": f"user_{i}",
"content": f"并发测试消息_{i}"
}) as response:
return response.status == 200
except ClientError:
return False
start_time = time.time()
tasks = [send_message(i) for i in range(100)]
results = await asyncio.gather(*tasks)
duration = time.time() - start_time
success_count = sum(results)
success_rate = success_count / len(results) * 100
assert success_rate > 95, f"成功率 {success_rate}% 低于95%"
assert duration < 10, f"耗时 {duration}s 超过10s"
print(f"并发测试完成: 成功率 {success_rate}%, 耗时 {duration:.2f}s")
七、总结
测试是保障微信机器人质量的关键:
- 单元测试:快速验证核心功能
- 集成测试:验证模块间协作
- 端到端测试:验证完整业务流程
- 性能测试:确保系统高并发能力
- 自动化流水线:持续保障代码质量
完善的测试体系可以让团队更有信心地交付高质量的产品。
本文仅用于技术交流和学习目的。