06. 测试最佳实践
🎯 本章目标
本章将介绍智能合约测试的最佳实践,包括:
- 测试策略和规划
- 测试代码质量
- 测试维护和更新
- 团队协作和代码审查
- 持续集成和部署
- 测试文档和知识管理
🎯 测试策略和规划
1. 测试金字塔模型
智能合约测试应该遵循测试金字塔模型:
/\
/ \ E2E Tests (少量)
/____\
/ \ Integration Tests (中等)
/________\
/ \ Unit Tests (大量)
/____________\
各层测试的特点:
-
单元测试 (Unit Tests): 测试单个函数或组件
- 数量最多,执行最快
- 覆盖所有公共函数
- 测试边界条件和错误情况
-
集成测试 (Integration Tests): 测试合约之间的交互
- 数量中等,执行时间中等
- 测试合约组合功能
- 测试外部依赖
-
端到端测试 (E2E Tests): 测试完整业务流程
- 数量最少,执行最慢
- 测试用户场景
- 测试部署和升级流程
2. 测试优先级矩阵
使用优先级矩阵来规划测试:
| 风险等级 | 业务重要性 | 测试优先级 | 测试策略 |
|---|---|---|---|
| 高 | 高 | P0 | 全面测试,包括边界条件、压力测试 |
| 高 | 中 | P1 | 核心功能测试,重点边界条件 |
| 中 | 高 | P1 | 功能完整性测试,用户场景测试 |
| 中 | 中 | P2 | 基本功能测试,主要路径测试 |
| 低 | 低 | P3 | 简单验证测试 |
3. 测试计划模板
# 测试计划
## 项目概述
- 项目名称: [项目名]
- 测试范围: [描述测试范围]
- 测试目标: [描述测试目标]
## 测试策略
- 测试类型: 单元测试、集成测试、端到端测试
- 测试工具: Hardhat、Chai、Mocha
- 测试环境: 本地开发环境、测试网
## 测试范围
### 功能测试
- [ ] 核心业务逻辑
- [ ] 权限控制
- [ ] 错误处理
- [ ] 事件触发
### 非功能测试
- [ ] Gas 优化
- [ ] 安全性测试
- [ ] 性能测试
## 测试进度
- [ ] 测试用例设计
- [ ] 测试用例实现
- [ ] 测试执行
- [ ] 缺陷修复
- [ ] 回归测试
## 风险评估
- 高风险功能: [列出高风险功能]
- 缓解措施: [描述缓解措施]
📝 测试代码质量
1. 代码规范
命名规范
// ✅ 好的命名
describe("Token Transfer", function () {
it("should transfer tokens when sender has sufficient balance", function () {});
it("should revert transfer when sender has insufficient balance", function () {});
it("should emit Transfer event after successful transfer", function () {});
});
// ❌ 不好的命名
describe("Test", function () {
it("test1", function () {});
it("test2", function () {});
it("test3", function () {});
});
结构规范
describe("Contract Name", function () {
// 1. 变量声明
let contract, owner, user1, user2;
// 2. 钩子函数
beforeEach(async function () {
[owner, user1, user2] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("ContractName");
contract = await Contract.deploy();
});
// 3. 测试套件
describe("Function Name", function () {
// 4. 测试用例
it("should work correctly", async function () {
// 5. AAA 结构
// Arrange
const input = 100;
// Act
const result = await contract.function(input);
// Assert
expect(result).to.equal(200);
});
});
});
2. 代码审查清单
测试结构审查
- 测试文件命名是否清晰?
- 测试套件组织是否合理?
- 测试用例是否独立?
- 是否使用了适当的钩子函数?
测试逻辑审查
- 测试用例是否覆盖了所有功能?
- 是否测试了边界条件?
- 是否测试了错误情况?
- 断言是否准确和有意义?
代码质量审查
- 代码是否遵循命名规范?
- 是否有重复代码?
- 是否使用了适当的测试工具?
- 错误消息是否清晰?
3. 代码重构示例
重构前:重复代码
describe("Token Tests", function () {
it("should transfer 100 tokens", async function () {
const [owner, user1] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy();
await token.transfer(user1.address, 100);
expect(await token.balanceOf(user1.address)).to.equal(100);
});
it("should transfer 200 tokens", async function () {
const [owner, user1] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy();
await token.transfer(user1.address, 200);
expect(await token.balanceOf(user1.address)).to.equal(200);
});
});
重构后:消除重复
describe("Token Tests", function () {
let token, owner, user1;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
token = await Token.deploy();
});
const transferTests = [
{ amount: 100, expected: 100 },
{ amount: 200, expected: 200 },
{ amount: 500, expected: 500 }
];
transferTests.forEach(({ amount, expected }) => {
it(`should transfer ${amount} tokens`, async function () {
await token.transfer(user1.address, amount);
expect(await token.balanceOf(user1.address)).to.equal(expected);
});
});
});
🔧 测试维护和更新
1. 测试维护策略
定期审查
- 每周: 检查测试执行结果
- 每月: 审查测试覆盖率
- 每季度: 评估测试策略有效性
测试更新
// 版本更新后的测试维护
describe("Contract v2", function () {
it("should maintain backward compatibility", async function () {
// 测试新功能
const result = await contract.newFunction();
expect(result).to.equal(expectedValue);
// 测试旧功能仍然工作
const oldResult = await contract.oldFunction();
expect(oldResult).to.equal(oldExpectedValue);
});
it("should handle new parameters correctly", async function () {
// 测试新参数
await expect(
contract.functionWithNewParam(newParam)
).to.not.be.reverted;
});
});
2. 测试数据管理
测试数据版本控制
// 测试数据配置文件
const testData = {
v1: {
users: [
{ name: "User1", balance: 1000 },
{ name: "User2", balance: 2000 }
],
settings: {
maxTransfer: 1000,
fee: 0.01
}
},
v2: {
users: [
{ name: "User1", balance: 1000, role: "admin" },
{ name: "User2", balance: 2000, role: "user" }
],
settings: {
maxTransfer: 2000,
fee: 0.005,
newFeature: true
}
}
};
module.exports = testData;
测试数据清理
describe("Data Management", function () {
afterEach(async function () {
// 清理测试数据
if (await contract.hasData()) {
await contract.cleanup();
}
});
it("should start with clean state", async function () {
expect(await contract.getDataCount()).to.equal(0);
});
});
👥 团队协作和代码审查
1. 代码审查流程
审查清单
# 测试代码审查清单
## 功能完整性
- [ ] 是否覆盖了所有主要功能?
- [ ] 是否测试了边界条件?
- [ ] 是否测试了错误情况?
## 代码质量
- [ ] 代码是否清晰易读?
- [ ] 是否遵循命名规范?
- [ ] 是否有重复代码?
## 测试有效性
- [ ] 测试用例是否独立?
- [ ] 断言是否准确?
- [ ] 是否使用了适当的测试工具?
## 维护性
- [ ] 测试是否易于维护?
- [ ] 是否使用了适当的抽象?
- [ ] 错误消息是否清晰?
审查反馈示例
// 审查前:测试不够清晰
it("test", async function () {
const result = await contract.function();
expect(result).to.be.true;
});
// 审查后:测试清晰明确
it("should return true when function executes successfully", async function () {
// Arrange
const expectedResult = true;
// Act
const result = await contract.function();
// Assert
expect(result).to.equal(expectedResult);
});
2. 团队协作工具
Git 工作流
# 功能分支工作流
git checkout -b feature/add-new-tests
git add .
git commit -m "Add comprehensive tests for new feature"
git push origin feature/add-new-tests
# 创建 Pull Request
# 团队成员审查代码
# 通过审查后合并到主分支
代码审查工具
- GitHub Pull Requests: 代码审查和讨论
- GitLab Merge Requests: 代码审查和CI/CD
- Bitbucket Pull Requests: 代码审查和协作
🚀 持续集成和部署
1. CI/CD 配置
GitHub Actions 配置
# .github/workflows/test.yml
name: Smart Contract Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Compile contracts
run: npx hardhat compile
- name: Run tests
run: npx hardhat test
- name: Generate coverage report
run: npx hardhat coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.json
GitLab CI 配置
# .gitlab-ci.yml
stages:
- test
- coverage
test:
stage: test
image: node:18
script:
- npm ci
- npx hardhat compile
- npx hardhat test
only:
- main
- develop
- merge_requests
coverage:
stage: coverage
image: node:18
script:
- npm ci
- npx hardhat coverage
coverage: '/All files[^|]*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\s+(\d+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
2. 测试自动化
自动化测试脚本
#!/bin/bash
# scripts/run-tests.sh
echo "🚀 Starting Smart Contract Tests..."
# 检查依赖
if ! command -v node &> /dev/null; then
echo "❌ Node.js is not installed"
exit 1
fi
if ! command -v npm &> /dev/null; then
echo "❌ npm is not installed"
exit 1
fi
# 安装依赖
echo "📦 Installing dependencies..."
npm ci
# 编译合约
echo "🔨 Compiling contracts..."
npx hardhat compile
# 运行测试
echo "🧪 Running tests..."
npx hardhat test
# 生成覆盖率报告
echo "📊 Generating coverage report..."
npx hardhat coverage
echo "✅ Tests completed successfully!"
测试结果通知
// 测试结果通知脚本
const { ethers } = require("hardhat");
async function notifyTestResults() {
try {
// 运行测试
await hre.run("test");
// 发送成功通知
console.log("🎉 All tests passed!");
// 可以集成到 Slack、Discord 等
// await sendSlackNotification("Tests passed successfully");
} catch (error) {
console.error("❌ Tests failed:", error.message);
// 发送失败通知
// await sendSlackNotification("Tests failed: " + error.message);
process.exit(1);
}
}
notifyTestResults();
📚 测试文档和知识管理
1. 测试文档模板
测试用例文档
# 测试用例文档
## 合约名称
[合约名称]
## 测试范围
[描述测试覆盖的功能]
## 测试用例
### TC001: 基本功能测试
**描述**: 测试合约的基本功能
**前置条件**: 合约已部署
**测试步骤**:
1. 调用基本函数
2. 验证返回值
3. 检查状态变化
**预期结果**: 函数执行成功,返回值正确
**实际结果**: [测试执行后填写]
**状态**: [通过/失败]
### TC002: 边界条件测试
**描述**: 测试合约在边界条件下的行为
**前置条件**: 合约已部署
**测试步骤**:
1. 使用最小值调用函数
2. 使用最大值调用函数
3. 使用零值调用函数
**预期结果**: 合约正确处理边界值
**实际结果**: [测试执行后填写]
**状态**: [通过/失败]
测试报告模板
# 测试执行报告
## 测试概述
- 测试日期: [日期]
- 测试环境: [环境描述]
- 测试范围: [测试范围描述]
## 测试结果摘要
- 总测试用例数: [数量]
- 通过: [数量]
- 失败: [数量]
- 跳过: [数量]
- 通过率: [百分比]%
## 详细结果
### 通过的测试
- [测试用例名称] - [执行时间]
- [测试用例名称] - [执行时间]
### 失败的测试
- [测试用例名称] - [失败原因]
- [测试用例名称] - [失败原因]
### 跳过的测试
- [测试用例名称] - [跳过原因]
## 问题总结
[列出发现的主要问题]
## 建议和改进
[提出改进建议]
2. 知识管理系统
测试知识库结构
docs/
├── testing/
│ ├── guidelines/ # 测试指南
│ │ ├── unit-testing.md
│ │ ├── integration-testing.md
│ │ └── e2e-testing.md
│ ├── examples/ # 测试示例
│ │ ├── token-tests.md
│ │ ├── voting-tests.md
│ │ └── upgrade-tests.md
│ ├── best-practices/ # 最佳实践
│ │ ├── test-organization.md
│ │ ├── test-maintenance.md
│ │ └── team-collaboration.md
│ └── troubleshooting/ # 问题解决
│ ├── common-issues.md
│ ├── debugging-tips.md
│ └── performance-optimization.md
知识分享流程
- 定期分享会: 每周团队分享测试经验
- 代码审查: 通过代码审查学习最佳实践
- 文档更新: 及时更新测试文档
- 培训计划: 新成员测试培训
📊 测试指标和监控
1. 关键测试指标
覆盖率指标
// 覆盖率监控脚本
const { execSync } = require('child_process');
function getCoverageMetrics() {
try {
// 运行覆盖率测试
const output = execSync('npx hardhat coverage', { encoding: 'utf8' });
// 解析覆盖率数据
const coverageMatch = output.match(/All files[^|]*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\s+(\d+)/);
if (coverageMatch) {
const coverage = parseInt(coverageMatch[1]);
console.log(`📊 Test Coverage: ${coverage}%`);
// 检查覆盖率阈值
if (coverage < 80) {
console.warn("⚠️ Coverage is below 80% threshold");
return false;
}
return true;
}
return false;
} catch (error) {
console.error("❌ Failed to get coverage metrics:", error.message);
return false;
}
}
// 导出覆盖率指标
module.exports = { getCoverageMetrics };
性能指标
// 性能监控脚本
async function measureTestPerformance() {
const startTime = Date.now();
try {
// 运行测试
await hre.run("test");
const endTime = Date.now();
const duration = endTime - startTime;
console.log(`⏱️ Test execution time: ${duration}ms`);
// 检查性能阈值
if (duration > 30000) { // 30秒
console.warn("⚠️ Test execution time exceeds 30 seconds");
}
return duration;
} catch (error) {
console.error("❌ Test execution failed:", error.message);
return -1;
}
}
module.exports = { measureTestPerformance };
2. 测试质量监控
质量检查脚本
// 测试质量检查
const fs = require('fs');
const path = require('path');
function checkTestQuality() {
const testDir = path.join(__dirname, '../test');
const testFiles = fs.readdirSync(testDir).filter(file => file.endsWith('.js'));
let totalTests = 0;
let totalDescribe = 0;
let qualityScore = 0;
testFiles.forEach(file => {
const content = fs.readFileSync(path.join(testDir, file), 'utf8');
// 统计测试数量
const testMatches = content.match(/it\(/g);
if (testMatches) {
totalTests += testMatches.length;
}
// 统计描述数量
const describeMatches = content.match(/describe\(/g);
if (describeMatches) {
totalDescribe += describeMatches.length;
}
// 检查代码质量
if (content.includes('console.log')) {
qualityScore += 1; // 包含调试代码
}
if (content.includes('// TODO')) {
qualityScore += 1; // 包含待办事项
}
});
console.log(`📈 Test Quality Report:`);
console.log(` Total Tests: ${totalTests}`);
console.log(` Total Describe: ${totalDescribe}`);
console.log(` Quality Score: ${qualityScore}/10`);
return {
totalTests,
totalDescribe,
qualityScore
};
}
module.exports = { checkTestQuality };
📚 总结
本章我们学习了:
- 测试策略: 测试金字塔、优先级矩阵、测试计划
- 代码质量: 代码规范、审查清单、重构技巧
- 测试维护: 维护策略、数据管理、版本控制
- 团队协作: 代码审查、协作工具、工作流程
- CI/CD: 自动化配置、测试脚本、结果通知
- 知识管理: 文档模板、知识库、分享流程
- 测试监控: 关键指标、质量检查、性能监控
关键要点:
- 策略先行: 制定清晰的测试策略和计划
- 质量为本: 保持测试代码的高质量
- 持续改进: 定期维护和更新测试
- 团队协作: 通过协作提高测试效果
- 自动化优先: 使用CI/CD自动化测试流程
- 知识积累: 建立完善的知识管理系统
- 监控反馈: 通过指标监控测试质量
在下一章中,我们将学习常见问题和解决方案,帮助解决测试过程中遇到的实际问题。
🧠 练习
- 为你的项目制定测试策略和计划
- 建立代码审查流程和清单
- 配置CI/CD自动化测试
- 创建测试文档和知识库