Day 20: 企业级自动化测试实战
学习目标
完成今天的学习后,你将能够:
- 设计完整的企业级测试项目架构
- 实现测试配置管理和数据驱动
- 集成CI/CD持续集成流程
- 生成专业的测试报告
技术原理
测试金字塔
/ E2E \
/ 集成测试 \
/ 单元测试 \
─────────────────企业级测试架构
project/
├── tests/
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ ├── api/ # API测试
│ ├── ui/ # UI测试
│ └── performance/ # 性能测试
├── conftest.py # pytest配置
├── pytest.ini # pytest配置
├── requirements.txt # 依赖
└── reports/ # 测试报告代码案例
案例1:项目结构设计
python
# conftest.py
"""
pytest全局配置
功能:定义全局fixture和配置
"""
import pytest
import json
from pathlib import Path
from typing import Dict, Any
from datetime import datetime
# 项目根目录
ROOT_DIR = Path(__file__).parent
CONFIG_DIR = ROOT_DIR / "config"
DATA_DIR = ROOT_DIR / "data"
REPORT_DIR = ROOT_DIR / "reports"
@pytest.fixture(scope="session")
def config() -> Dict[str, Any]:
"""加载测试配置"""
config_file = CONFIG_DIR / "test_config.json"
if config_file.exists():
with open(config_file, 'r', encoding='utf-8') as f:
return json.load(f)
# 默认配置
return {
'environment': 'test',
'base_url': 'https://jsonplaceholder.typicode.com',
'timeout': 30,
'retry_count': 3
}
@pytest.fixture(scope="session")
def test_data() -> Dict[str, Any]:
"""加载测试数据"""
data_file = DATA_DIR / "test_data.json"
if data_file.exists():
with open(data_file, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
@pytest.fixture(scope="function")
def test_name(request):
"""获取测试名称"""
return request.node.name
@pytest.fixture(scope="function")
def test_timestamp():
"""获取测试时间戳"""
return datetime.now().strftime("%Y%m%d_%H%M%S")
@pytest.fixture(scope="session", autouse=True)
def setup_report_dir():
"""创建报告目录"""
REPORT_DIR.mkdir(exist_ok=True)
yield
print(f"\n测试报告目录: {REPORT_DIR}")
# pytest配置
def pytest_configure(config):
"""pytest配置钩子"""
# 添加自定义标记
config.addinivalue_line("markers", "smoke: 冒烟测试")
config.addinivalue_line("markers", "regression: 回归测试")
config.addinivalue_line("markers", "slow: 慢速测试")
def pytest_collection_modifyitems(items):
"""修改测试收集"""
# 为慢速测试添加标记
for item in items:
if "slow" in item.nodeid:
item.add_marker(pytest.mark.slow)ini
# pytest.ini
[pytest]
# 测试路径
testpaths = tests
# 命令行参数
addopts =
-v
--tb=short
--strict-markers
-m "not slow"
# 标记
markers =
smoke: 冒烟测试
regression: 回归测试
slow: 慢速测试
# 日志
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format = %Y-%m-%d %H:%M:%S
# 报告
junitxml = reports/test-results.xml案例2:API 测试框架
python
# tests/api/test_posts.py
"""
API测试用例
功能:测试文章相关API
"""
import pytest
import requests
from typing import Dict, List
class TestPostsAPI:
"""文章API测试"""
@pytest.fixture(autouse=True)
def setup(self, config):
"""初始化"""
self.base_url = config['base_url']
self.timeout = config['timeout']
self.session = requests.Session()
self.session.headers.update({
'Content-Type': 'application/json'
})
def teardown_method(self):
"""清理"""
self.session.close()
@pytest.mark.smoke
def test_get_all_posts(self):
"""测试获取所有文章"""
response = self.session.get(
f"{self.base_url}/posts",
timeout=self.timeout
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
# 验证数据结构
post = data[0]
assert 'userId' in post
assert 'id' in post
assert 'title' in post
assert 'body' in post
@pytest.mark.smoke
def test_get_post_by_id(self):
"""测试根据ID获取文章"""
post_id = 1
response = self.session.get(
f"{self.base_url}/posts/{post_id}",
timeout=self.timeout
)
assert response.status_code == 200
data = response.json()
assert data['id'] == post_id
assert 'title' in data
assert 'body' in data
@pytest.mark.regression
@pytest.mark.parametrize("post_id", [1, 2, 3, 5, 10])
def test_get_multiple_posts(self, post_id: int):
"""参数化测试多篇文章"""
response = self.session.get(
f"{self.base_url}/posts/{post_id}",
timeout=self.timeout
)
assert response.status_code == 200
assert response.json()['id'] == post_id
@pytest.mark.regression
def test_create_post(self):
"""测试创建文章"""
payload = {
'title': '测试文章',
'body': '测试内容',
'userId': 1
}
response = self.session.post(
f"{self.base_url}/posts",
json=payload,
timeout=self.timeout
)
assert response.status_code == 201
data = response.json()
assert data['title'] == payload['title']
assert data['body'] == payload['body']
assert 'id' in data
@pytest.mark.regression
def test_update_post(self):
"""测试更新文章"""
post_id = 1
payload = {
'title': '更新后的标题',
'body': '更新后的内容',
'userId': 1
}
response = self.session.put(
f"{self.base_url}/posts/{post_id}",
json=payload,
timeout=self.timeout
)
assert response.status_code == 200
data = response.json()
assert data['title'] == payload['title']
@pytest.mark.regression
def test_delete_post(self):
"""测试删除文章"""
post_id = 1
response = self.session.delete(
f"{self.base_url}/posts/{post_id}",
timeout=self.timeout
)
assert response.status_code == 200
def test_get_post_comments(self):
"""测试获取文章评论"""
post_id = 1
response = self.session.get(
f"{self.base_url}/posts/{post_id}/comments",
timeout=self.timeout
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
# 验证评论属于该文章
for comment in data:
assert comment['postId'] == post_id
def test_filter_posts_by_user(self):
"""测试按用户筛选文章"""
user_id = 1
response = self.session.get(
f"{self.base_url}/posts",
params={'userId': user_id},
timeout=self.timeout
)
assert response.status_code == 200
data = response.json()
assert all(post['userId'] == user_id for post in data)
class TestPostsAPIEdgeCases:
"""API边界条件测试"""
@pytest.fixture(autouse=True)
def setup(self, config):
self.base_url = config['base_url']
self.timeout = config['timeout']
def test_get_nonexistent_post(self):
"""测试获取不存在的文章"""
response = requests.get(
f"{self.base_url}/posts/99999",
timeout=self.timeout
)
assert response.status_code == 404
def test_create_post_invalid_data(self):
"""测试使用无效数据创建文章"""
payload = {} # 空数据
response = requests.post(
f"{self.base_url}/posts",
json=payload,
timeout=self.timeout
)
# JSONPlaceholder不会验证,实际项目应返回400
assert response.status_code == 201
# tests/api/conftest.py
"""
API测试配置
"""
import pytest
import requests
@pytest.fixture(scope="session")
def api_client(config):
"""创建API客户端"""
class APIClient:
def __init__(self, base_url: str, timeout: int = 10):
self.base_url = base_url
self.session = requests.Session()
self.timeout = timeout
def get(self, endpoint: str, **kwargs):
return self.session.get(
f"{self.base_url}{endpoint}",
timeout=self.timeout,
**kwargs
)
def post(self, endpoint: str, **kwargs):
return self.session.post(
f"{self.base_url}{endpoint}",
timeout=self.timeout,
**kwargs
)
def put(self, endpoint: str, **kwargs):
return self.session.put(
f"{self.base_url}{endpoint}",
timeout=self.timeout,
**kwargs
)
def delete(self, endpoint: str, **kwargs):
return self.session.delete(
f"{self.base_url}{endpoint}",
timeout=self.timeout,
**kwargs
)
def close(self):
self.session.close()
client = APIClient(config['base_url'], config['timeout'])
yield client
client.close()案例3:测试配置管理
python
# config/test_config.json
"""
测试配置文件
"""
{
"environment": "test",
"base_url": "https://jsonplaceholder.typicode.com",
"timeout": 30,
"retry_count": 3,
"browser": {
"type": "chromium",
"headless": true,
"viewport": {
"width": 1920,
"height": 1080
}
},
"database": {
"host": "localhost",
"port": 5432,
"name": "test_db"
},
"report": {
"format": "html",
"output_dir": "reports"
}
}python
# config/__init__.py
"""
配置管理模块
"""
import json
from pathlib import Path
from typing import Any, Optional
from dataclasses import dataclass
import os
@dataclass
class BrowserConfig:
"""浏览器配置"""
type: str = "chromium"
headless: bool = True
viewport_width: int = 1920
viewport_height: int = 1080
@dataclass
class DatabaseConfig:
"""数据库配置"""
host: str = "localhost"
port: int = 5432
name: str = "test_db"
user: str = ""
password: str = ""
class TestConfig:
"""测试配置管理器"""
_instance = None
_config = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def load(self, config_path: str = None):
"""加载配置"""
if config_path is None:
config_path = Path(__file__).parent / "test_config.json"
with open(config_path, 'r', encoding='utf-8') as f:
self._config = json.load(f)
# 环境变量覆盖
self._apply_env_overrides()
return self
def _apply_env_overrides(self):
"""应用环境变量覆盖"""
env_mappings = {
'TEST_BASE_URL': 'base_url',
'TEST_TIMEOUT': 'timeout',
'TEST_ENVIRONMENT': 'environment'
}
for env_key, config_key in env_mappings.items():
env_value = os.getenv(env_key)
if env_value:
# 类型转换
if config_key == 'timeout':
self._config[config_key] = int(env_value)
else:
self._config[config_key] = env_value
def get(self, key: str, default: Any = None) -> Any:
"""获取配置项"""
if self._config is None:
self.load()
return self._config.get(key, default)
@property
def base_url(self) -> str:
return self.get('base_url', 'https://jsonplaceholder.typicode.com')
@property
def timeout(self) -> int:
return self.get('timeout', 30)
@property
def environment(self) -> str:
return self.get('environment', 'test')
@property
def browser(self) -> BrowserConfig:
browser_data = self.get('browser', {})
return BrowserConfig(
type=browser_data.get('type', 'chromium'),
headless=browser_data.get('headless', True),
viewport_width=browser_data.get('viewport', {}).get('width', 1920),
viewport_height=browser_data.get('viewport', {}).get('height', 1080)
)
def to_dict(self) -> dict:
"""转换为字典"""
return self._config or {}
# 全局配置实例
config = TestConfig()案例4:测试报告生成
python
# reports/report_generator.py
"""
测试报告生成器
功能:生成HTML和JSON格式的测试报告
"""
import json
from datetime import datetime
from typing import List, Dict, Any
from pathlib import Path
from dataclasses import dataclass, asdict
@dataclass
class TestCaseResult:
"""测试用例结果"""
name: str
status: str # passed, failed, error, skipped
duration: float
message: str = ""
traceback: str = ""
@dataclass
class TestSuiteResult:
"""测试套件结果"""
name: str
tests: List[TestCaseResult]
start_time: str
end_time: str
duration: float
@property
def total(self) -> int:
return len(self.tests)
@property
def passed(self) -> int:
return sum(1 for t in self.tests if t.status == 'passed')
@property
def failed(self) -> int:
return sum(1 for t in self.tests if t.status == 'failed')
@property
def error(self) -> int:
return sum(1 for t in self.tests if t.status == 'error')
@property
def skipped(self) -> int:
return sum(1 for t in self.tests if t.status == 'skipped')
@property
def pass_rate(self) -> str:
if self.total == 0:
return "0%"
return f"{(self.passed / self.total * 100):.1f}%"
class ReportGenerator:
"""测试报告生成器"""
def __init__(self, output_dir: str = "reports"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
def generate_json_report(self, suite: TestSuiteResult,
filename: str = None) -> str:
"""生成JSON报告"""
if filename is None:
filename = f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
filepath = self.output_dir / filename
report_data = {
'summary': {
'total': suite.total,
'passed': suite.passed,
'failed': suite.failed,
'error': suite.error,
'skipped': suite.skipped,
'pass_rate': suite.pass_rate,
'duration': f"{suite.duration:.2f}s"
},
'start_time': suite.start_time,
'end_time': suite.end_time,
'tests': [asdict(t) for t in suite.tests]
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(report_data, f, ensure_ascii=False, indent=2)
print(f"JSON报告已生成: {filepath}")
return str(filepath)
def generate_html_report(self, suite: TestSuiteResult,
filename: str = None) -> str:
"""生成HTML报告"""
if filename is None:
filename = f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = self.output_dir / filename
html_content = self._generate_html_content(suite)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"HTML报告已生成: {filepath}")
return str(filepath)
def _generate_html_content(self, suite: TestSuiteResult) -> str:
"""生成HTML内容"""
test_rows = ""
for test in suite.tests:
status_class = f"status-{test.status}"
test_rows += f"""
<tr>
<td>{test.name}</td>
<td class="{status_class}">{test.status.upper()}</td>
<td>{test.duration:.3f}s</td>
<td>{test.message}</td>
</tr>
"""
return f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试报告</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px 8px 0 0;
}}
.summary {{
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
padding: 30px;
}}
.summary-item {{
text-align: center;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}}
.summary-item h3 {{
margin: 0;
font-size: 2em;
color: #333;
}}
.summary-item p {{
margin: 5px 0 0;
color: #666;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 0 30px 30px;
width: calc(100% - 60px);
}}
th, td {{
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}}
th {{
background: #f8f9fa;
font-weight: 600;
}}
.status-passed {{ color: #28a745; font-weight: bold; }}
.status-failed {{ color: #dc3545; font-weight: bold; }}
.status-error {{ color: #fd7e14; font-weight: bold; }}
.status-skipped {{ color: #6c757d; font-weight: bold; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>自动化测试报告</h1>
<p>生成时间: {suite.end_time}</p>
</div>
<div class="summary">
<div class="summary-item">
<h3>{suite.total}</h3>
<p>总用例数</p>
</div>
<div class="summary-item">
<h3 style="color: #28a745">{suite.passed}</h3>
<p>通过</p>
</div>
<div class="summary-item">
<h3 style="color: #dc3545">{suite.failed}</h3>
<p>失败</p>
</div>
<div class="summary-item">
<h3 style="color: #fd7e14">{suite.error}</h3>
<p>错误</p>
</div>
<div class="summary-item">
<h3>{suite.pass_rate}</h3>
<p>通过率</p>
</div>
</div>
<table>
<thead>
<tr>
<th>测试用例</th>
<th>状态</th>
<th>耗时</th>
<th>信息</th>
</tr>
</thead>
<tbody>
{test_rows}
</tbody>
</table>
</div>
</body>
</html>
"""
def demo_report_generation():
"""演示报告生成"""
# 创建测试结果
tests = [
TestCaseResult("test_get_posts", "passed", 0.123),
TestCaseResult("test_create_post", "passed", 0.234),
TestCaseResult("test_update_post", "passed", 0.156),
TestCaseResult("test_delete_post", "failed", 0.089, "状态码不匹配"),
TestCaseResult("test_get_comments", "passed", 0.167),
]
suite = TestSuiteResult(
name="API测试套件",
tests=tests,
start_time=datetime.now().isoformat(),
end_time=datetime.now().isoformat(),
duration=0.769
)
# 生成报告
generator = ReportGenerator("reports")
json_path = generator.generate_json_report(suite)
html_path = generator.generate_html_report(suite)
print(f"\n测试摘要:")
print(f" 总用例: {suite.total}")
print(f" 通过: {suite.passed}")
print(f" 失败: {suite.failed}")
print(f" 通过率: {suite.pass_rate}")
if __name__ == "__main__":
demo_report_generation()案例5:CI/CD 集成
yaml
# .github/workflows/test.yml
"""
GitHub Actions测试工作流
"""
name: 自动化测试
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11]
steps:
- uses: actions/checkout@v3
- name: 设置Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: 安装依赖
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov pytest-html
- name: 运行单元测试
run: |
pytest tests/unit/ -v --cov=src --cov-report=xml
- name: 运行API测试
run: |
pytest tests/api/ -v --html=reports/api-report.html
- name: 上传测试报告
uses: actions/upload-artifact@v3
if: always()
with:
name: test-reports
path: reports/
- name: 上传覆盖率报告
uses: codecov/codecov-action@v3
if: always()
with:
file: ./coverage.xmltxt
# requirements.txt
# 测试依赖
pytest>=7.0.0
pytest-cov>=4.0.0
pytest-html>=3.2.0
pytest-ordering>=0.6
pytest-xdist>=3.0.0
# API测试
requests>=2.28.0
httpx>=0.24.0
# 压测
locust>=2.13.0
# UI测试
playwright>=1.40.0
selenium>=4.10.0
# 报告
jinja2>=3.1.0课后练习
练习1:项目架构
- 设计一个完整的测试项目结构
- 实现配置管理模块
- 编写测试数据管理
练习2:测试套件
- 编写至少10个测试用例
- 实现数据驱动测试
- 添加测试标记和筛选
练习3:CI/CD集成
- 配置GitHub Actions工作流
- 实现自动化测试执行
- 集成测试报告生成
常见问题
Q1: 如何组织测试用例?
A: 按功能模块组织,使用测试类分组,遵循Arrange-Act-Assert模式。
Q2: 如何管理测试数据?
A: 使用JSON/YAML文件存储,通过fixture加载,支持环境切换。
Q3: 如何提高测试执行速度?
A: 使用pytest-xdist并行执行,优化测试依赖,减少I/O操作。
总结
恭喜你完成了Python自动化系列的全部学习!
学习成果
通过20天的学习,你已经掌握了:
- 基础自动化:Python脚本、文件处理、系统任务
- 进阶自动化:日志记录、测试、部署、CI/CD
- 实战自动化:监控告警、日志分析、AI自动化、网页自动化、爬虫
- API测试:接口自动化测试框架
- 性能测试:压测和性能分析
- UI测试:Playwright和Selenium
- 脚本自动化:文件批处理、办公自动化
- 企业级实践:完整测试项目架构
下一步建议
- 项目实践:将所学应用到实际项目
- 深入学习:选择感兴趣的领域深入研究
- 社区参与:参与开源项目,分享经验
- 持续学习:关注新技术,保持学习
推荐资源
祝你在自动化的道路上越走越远!