在基于 OpenClaw + Claude Code 的端到端研发自动化系统中,单元测试作为质量保证的第一道防线, 承担着验证代码正确性、防止回归缺陷、支持重构的关键职责。本规范旨在建立统一的单元测试标准, 确保在 AI 辅助编程时代,测试质量与开发效率同步提升。
建立统一的测试用例编写规范,确保不同角色(人类开发者、AI Agent)产出的测试具有一致的质量标准。
通过明确的覆盖率指标和质量门禁,实现测试完整性的客观评估和持续改进。
支持 AI 自动生成测试用例、自动执行、自动报告,实现人机协同的高效测试流程。
测试代码与业务代码同等重要,易于维护、扩展,随产品迭代持续演进。
本规范适用于端到端研发自动化系统中的以下环节:
基于 ANSI/IEEE 829 标准和行业最佳实践,每个单元测试用例必须包含以下八大核心要素。 该模板同时适用于人工编写和 AI 自动生成的测试用例。
唯一标识符,格式:[项目]-[测试类型]-[模块]-[功能]-[序号]
示例:CRM-UT-User-Create-001被测功能模块的层级描述,精确到子功能级别
示例:用户管理 → 新增用户简洁描述测试目的,每条用例对应一个验证点
示例:验证用户名包含特殊字符时的输入校验P0-关键路径 / P1-核心功能 / P2-一般功能 / P3-边缘场景
示例:P0 - 登录认证必须通过执行测试前必须满足的环境和数据状态
示例:数据库连接正常,测试用户不存在提供给被测函数的参数、数据或外部依赖
示例:username="test_user", age=25清晰的执行步骤序列,支持自动化脚本转换
示例:1.调用 createUser() 方法明确的断言条件,包括返回值、状态变更、异常等
示例:返回用户 ID,数据库存在对应记录# ============================================
# 单元测试用例标准模板
# ============================================
# 1. 用例编号
test_id = "ORDER-UT-Payment-Process-001"
# 2. 测试项
test_item = "订单管理 → 支付处理 → 支付宝支付"
# 3. 标题
title = "验证支付金额大于账户余额时抛出 InsufficientFundsException"
# 4. 重要级别
priority = "P0" # 关键路径测试
# 5. 预置条件
preconditions = [
"用户账户已创建且余额为 100 元",
"订单状态为待支付",
"支付网关 Mock 服务已启动"
]
# 6. 测试输入
test_input = {
"user_id": "user_12345",
"order_id": "order_67890",
"payment_amount": 150.00, # 大于账户余额
"payment_method": "alipay"
}
# 7. 操作步骤
steps = [
"1. 获取用户账户信息",
"2. 调用 PaymentService.process() 方法",
"3. 捕获并验证异常类型",
"4. 验证订单状态未变更"
]
# 8. 预期结果
expected_result = {
"exception_type": "InsufficientFundsException",
"order_status": "PENDING", # 保持待支付状态
"account_balance": 100.00, # 余额不变
"error_code": "PAYMENT_FAILED_INSUFFICIENT_FUNDS"
}所有单元测试必须遵循 3A 原则,确保测试结构清晰、意图明确、易于维护。
import pytest
from services.payment_service import PaymentService
from exceptions import InsufficientFundsException
class TestPaymentService:
# ==================== Arrange ====================
def setup_method(self):
# 初始化测试对象和 Mock 依赖
self.mock_account_repo = MockAccountRepository()
self.mock_order_repo = MockOrderRepository()
self.payment_service = PaymentService(
account_repo=self.mock_account_repo,
order_repo=self.mock_order_repo
)
# 准备测试数据
self.test_user_id = "user_12345"
self.test_order_id = "order_67890"
self.initial_balance = 100.00
# 设置 Mock 行为
self.mock_account_repo.get_balance.return_value = self.initial_balance
# ==================== Test Cases ====================
def test_payment_insufficient_funds(self):
# --- Arrange ---
payment_amount = 150.00 # 大于余额
# --- Act ---
with pytest.raises(InsufficientFundsException) as exc_info:
self.payment_service.process(
user_id=self.test_user_id,
order_id=self.test_order_id,
amount=payment_amount
)
# --- Assert ---
# 验证异常类型
assert exc_info.type == InsufficientFundsException
# 验证错误码
assert exc_info.value.error_code == "PAYMENT_FAILED_INSUFFICIENT_FUNDS"
# 验证账户余额未变更
self.mock_account_repo.get_balance.assert_called_with(self.test_user_id)
# 验证订单状态未更新
self.mock_order_repo.update_status.assert_not_called()
def test_payment_success(self):
# --- Arrange ---
payment_amount = 50.00 # 小于余额
expected_new_balance = 50.00
# --- Act ---
result = self.payment_service.process(
user_id=self.test_user_id,
order_id=self.test_order_id,
amount=payment_amount
)
# --- Assert ---
assert result.success == True
assert result.transaction_id is not None
# 验证余额扣减
self.mock_account_repo.debit.assert_called_once_with(
self.test_user_id,
payment_amount
)
# 验证订单状态更新
self.mock_order_repo.update_status.assert_called_once_with(
self.test_order_id,
"PAID"
)对于需要多组输入数据的场景,使用参数化测试提高覆盖率:
@pytest.mark.parametrize("amount, expected_exception", [
(0, "InvalidAmountException"), # 边界值:0
(-10, "InvalidAmountException"), # 负数
(100.01, "InsufficientFundsException"), # 略大于余额
(999999, "InsufficientFundsException"), # 远大于余额
])
def test_payment_invalid_amount_scenarios(self, amount, expected_exception):
# --- Act & Assert ---
with pytest.raises(eval(expected_exception)):
self.payment_service.process(
user_id=self.test_user_id,
order_id=self.test_order_id,
amount=amount
)代码覆盖率是衡量测试完整性的核心指标。本规范定义以下覆盖率类型:
| 覆盖率类型 | 定义说明 | 测量维度 | 适用场景 |
|---|---|---|---|
| 行覆盖率 (Line Coverage) | 已执行的代码行数占总行数的百分比 | 语句级别 | 基础质量门禁 |
| 分支覆盖率 (Branch Coverage) | 控制结构的每个分支(true/false)是否都被执行 | 条件判断 | 逻辑复杂的核心模块 |
| 函数覆盖率 (Function Coverage) | 被调用的函数占函数总数的百分比 | 方法级别 | API 接口层 |
| 路径覆盖率 (Path Coverage) | 所有可能的执行路径被覆盖的比例 | 路径组合 | 安全关键系统 |
| 边界值覆盖率 | 边界条件和极端情况是否被测试 | 边界分析 | 数值计算、输入校验 |
根据模块的重要性和风险等级,设定不同的覆盖率门槛:
| 模块等级 | 行覆盖率 | 分支覆盖率 | 函数覆盖率 | 适用模块示例 |
|---|---|---|---|---|
| 🔴 关键核心 | ≥ 95% | ≥ 90% | 100% | 支付结算、身份认证、数据安全、风控引擎 |
| 🟠 重要业务 | ≥ 85% | ≥ 75% | 100% | 订单管理、库存管理、消息推送、报表统计 |
| 🔵 一般功能 | ≥ 75% | ≥ 60% | ≥ 90% | 用户配置、日志记录、缓存管理、定时任务 |
| 🟣 辅助工具 | ≥ 60% | ≥ 40% | ≥ 80% | 工具类、常量定义、DTO/VO 转换、配置读取 |
豁免流程:需在代码中添加 @ExcludeFromCoverage(reason="具体原因") 注解,并在 Code Review 中说明理由。
# Jenkins Pipeline 中的覆盖率检查配置
pipeline {
agent any
stages {
stage('Unit Test') {
steps {
script {
# 执行测试并生成覆盖率报告
sh 'pytest --cov=src --cov-report=xml --cov-report=html'
# 检查覆盖率是否达标
def coverageReport = readXML 'coverage.xml'
def lineCoverage = coverageReport.'@line-rate' * 100
def branchCoverage = coverageReport.'@branch-rate' * 100
# 质量门禁:关键模块覆盖率不达标则失败
if (lineCoverage < 85 || branchCoverage < 75) {
error "覆盖率不达标!行覆盖率:${lineCoverage}%, 分支覆盖率:${branchCoverage}%"
}
}
}
post {
always {
# 发布覆盖率报告
publishCoverage adapters: [coberturaAdapter('coverage.xml')]
}
}
}
}
}在 OpenClaw + Claude Code 的研发自动化系统中,AI 可以自动生成高质量的单元测试:
Claude Code 分析业务代码逻辑,自动生成符合 3A 原则的测试用例,覆盖正常流程和异常场景。
开发者在业务代码中添加测试提示注释,指导 AI 生成针对性的测试场景。
当业务代码变更时,AI 自动识别受影响的测试用例并进行更新。
生成的测试用例会自动执行,确保测试本身是正确的(无假阳性/假阴性)。
# ============================================
# Claude Code 测试生成 Prompt 模板
# ============================================
You are an expert software test engineer specializing in unit testing.
Your task is to generate comprehensive unit tests for the following code.
## Requirements:
1. Follow the **3A principle** (Arrange-Act-Assert)
2. Achieve **minimum 85% line coverage** and **75% branch coverage**
3. Include test cases for:
- Normal/happy path scenarios
- Edge cases and boundary conditions
- Error/exception handling
- Null/empty input validation
4. Use **pytest** framework with proper fixtures and mocks
5. Add descriptive test names that explain the scenario being tested
6. Include parameterized tests where applicable
## Code to Test:
```python
{CODE_TO_TEST}
```
## Output Format:
Generate tests in the following structure:
- Test class with setup_method for common initialization
- Individual test methods following naming convention: test_[scenario]_[expected_behavior]
- Clear comments explaining complex test logic
- Mock external dependencies appropriately
## Coverage Targets:
- Critical modules: ≥95% line, ≥90% branch
- Business modules: ≥85% line, ≥75% branch
- Utility modules: ≥75% line, ≥60% branchOpenClaw 作为 24/7 在线的 AI 助手,可以在以下环节自动化单元测试流程:
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: python
image: python:3.12
command:
- cat
tty: true
- name: docker
image: docker:24.0
command:
- cat
tty: true
"""
}
}
environment {
COVERAGE_THRESHOLD_LINE = '85'
COVERAGE_THRESHOLD_BRANCH = '75'
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://github.com/org/repo.git'
}
}
stage('Install Dependencies') {
steps {
container('python') {
sh 'pip install -r requirements.txt'
sh 'pip install pytest pytest-cov pytest-mock'
}
}
}
stage('AI Generate Tests') {
steps {
container('python') {
script {
# 调用 OpenClaw + Claude Code 生成测试
sh 'openclaw generate-tests --path src/ --output tests/'
}
}
}
}
stage('Run Unit Tests') {
steps {
container('python') {
sh 'pytest tests/ \
--cov=src \
--cov-report=xml:coverage.xml \
--cov-report=html:coverage-report \
--cov-report=term-missing \
--junitxml=test-results.xml \
-v'
}
}
}
stage('Coverage Gate') {
steps {
script {
def report = readXML 'coverage.xml'
def lineCov = (report.'@line-rate'.toFloat() * 100).round(2)
def branchCov = (report.'@branch-rate'.toFloat() * 100).round(2)
echo "Line Coverage: ${lineCov}%"
echo "Branch Coverage: ${branchCov}%"
if (lineCov < env.COVERAGE_THRESHOLD_LINE.toFloat()) {
error "❌ Line coverage ${lineCov}% is below threshold ${env.COVERAGE_THRESHOLD_LINE}%"
}
if (branchCov < env.COVERAGE_THRESHOLD_BRANCH.toFloat()) {
error "❌ Branch coverage ${branchCov}% is below threshold ${env.COVERAGE_THRESHOLD_BRANCH}%"
}
echo "✅ Coverage gates passed!"
}
}
}
stage('Build Docker Image') {
steps {
container('docker') {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
}
}
stage('Deploy to K8S') {
when {
branch 'main'
}
steps {
container('docker') {
sh 'kubectl apply -f k8s/deployment.yaml'
sh 'kubectl rollout status deployment/myapp'
}
}
}
}
post {
always {
junit 'test-results.xml'
publishCoverage adapters: [coberturaAdapter('coverage.xml')]
archiveArtifacts artifacts: 'coverage-report/**/*'
}
failure {
script {
# 发送失败通知到 Slack/钉钉
sh 'openclaw notify --channel ci-alerts --message "Build ${BUILD_NUMBER} failed"'
}
}
}
}| 角色 | 职责 | AI 辅助 |
|---|---|---|
| 人类开发者 | 定义测试策略、审查 AI 生成的测试、处理复杂边界场景、编写集成测试 | Claude Code 提供测试建议、自动生成重复性测试代码 |
| Claude Code | 根据业务代码自动生成单元测试、识别未覆盖的代码路径、推荐测试数据 | OpenClaw 调度执行、持久化记忆学习历史测试模式 |
| OpenClaw | 24/7 监控代码变更、自动触发测试生成、执行测试、报告结果、修复失败的测试 | Skills 系统扩展测试能力、跨平台通知、长期记忆优化 |
# 通过 OpenClaw 与 AI 协作生成测试
# 1. 为指定模块生成测试
$ openclaw "generate unit tests for src/payment_service.py with 90% coverage target"
# 2. 检查当前覆盖率
$ openclaw "check test coverage for the last commit"
# 3. 找出未覆盖的代码路径
$ openclaw "show me uncovered branches in src/order_module.py"
# 4. 为特定函数添加边界测试
$ openclaw "add edge case tests for calculate_discount() function"
# 5. 修复失败的测试
$ openclaw "fix failing tests in tests/test_payment.py"
# 6. 生成测试报告
$ openclaw "generate HTML test report and send to team channel"# -*- coding: utf-8 -*-
"""
@Project: CRM System
@Module: tests/test_user_service.py
@Author: [Developer Name]
@Created: 2026-03-14
@Description: 用户服务单元测试套件
@Test Coverage Target: Line ≥85%, Branch ≥75%
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from services.user_service import UserService
from models.user import User
from exceptions import (
UserNotFoundException,
DuplicateUserException,
InvalidEmailException
)
class TestUserService:
"""用户服务测试类"""
def setup_method(self):
"""每个测试前的准备工作"""
self.mock_user_repo = Mock()
self.mock_email_service = Mock()
self.user_service = UserService(
user_repo=self.mock_user_repo,
email_service=self.mock_email_service
)
self.test_user_data = {
"username": "test_user",
"email": "test@example.com",
"age": 25
}
def teardown_method(self):
"""每个测试后的清理工作"""
pass
# ========== 正常流程测试 ==========
def test_create_user_success(self):
"""测试成功创建用户"""
# Arrange
expected_user = User(id=1, **self.test_user_data)
self.mock_user_repo.exists.return_value = False
self.mock_user_repo.create.return_value = expected_user
# Act
result = self.user_service.create_user(**self.test_user_data)
# Assert
assert result.id == 1
assert result.username == "test_user"
self.mock_user_repo.create.assert_called_once()
self.mock_email_service.send_welcome_email.assert_called_once()
# ========== 异常场景测试 ==========
def test_create_user_duplicate_username(self):
"""测试创建重复用户名的用户"""
# Arrange
self.mock_user_repo.exists.return_value = True
# Act & Assert
with pytest.raises(DuplicateUserException):
self.user_service.create_user(**self.test_user_data)
def test_create_user_invalid_email(self):
"""测试创建用户使用无效邮箱"""
# Arrange
invalid_email_data = {**self.test_user_data, "email": "invalid-email"}
# Act & Assert
with pytest.raises(InvalidEmailException):
self.user_service.create_user(**invalid_email_data)
# ========== 边界值测试 ==========
@pytest.mark.parametrize("age, expected_valid", [
(-1, False), # 负数年龄
(0, True), # 边界:0 岁
(150, True), # 边界:最大合理年龄
(151, False), # 超过合理年龄
])
def test_create_user_age_boundaries(self, age, expected_valid):
"""测试年龄边界条件"""
# Arrange
test_data = {**self.test_user_data, "age": age}
# Act & Assert
if expected_valid:
self.mock_user_repo.exists.return_value = False
result = self.user_service.create_user(**test_data)
assert result is not None
else:
with pytest.raises(ValueError):
self.user_service.create_user(**test_data)
# ========== 集成场景测试 ==========
def test_delete_user_cascade(self):
"""测试删除用户时的级联操作"""
# Arrange
user_id = 123
self.mock_user_repo.get.return_value = User(id=user_id, **self.test_user_data)
# Act
self.user_service.delete_user(user_id)
# Assert
self.mock_user_repo.delete.assert_called_once_with(user_id)
# 验证关联数据也被清理
self.mock_user_repo.delete_related_orders.assert_called_once_with(user_id)
self.mock_user_repo.delete_related_logs.assert_called_once_with(user_id)
if __name__ == "__main__":
pytest.main([__file__, "-v", "--cov", "--cov-report=html"])| 工具类别 | Python | Java | JavaScript |
|---|---|---|---|
| 测试框架 | pytest, unittest | JUnit 5, TestNG | Jest, Mocha, Vitest |
| Mock 库 | unittest.mock, pytest-mock | Mockito, EasyMock | Jest Mock, Sinon |
| 覆盖率工具 | coverage.py, pytest-cov | JaCoCo, Cobertura | Istanbul, c8 |
| 数据工厂 | factory_boy, faker | Java Faker | Faker.js |
| 断言库 | pytest assert, assertpy | AssertJ, Hamcrest | Chai, Expect |