跳到主要内容

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. 定期分享会: 每周团队分享测试经验
  2. 代码审查: 通过代码审查学习最佳实践
  3. 文档更新: 及时更新测试文档
  4. 培训计划: 新成员测试培训

📊 测试指标和监控

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: 自动化配置、测试脚本、结果通知
  • 知识管理: 文档模板、知识库、分享流程
  • 测试监控: 关键指标、质量检查、性能监控

关键要点:

  1. 策略先行: 制定清晰的测试策略和计划
  2. 质量为本: 保持测试代码的高质量
  3. 持续改进: 定期维护和更新测试
  4. 团队协作: 通过协作提高测试效果
  5. 自动化优先: 使用CI/CD自动化测试流程
  6. 知识积累: 建立完善的知识管理系统
  7. 监控反馈: 通过指标监控测试质量

在下一章中,我们将学习常见问题和解决方案,帮助解决测试过程中遇到的实际问题。


🧠 练习

  1. 为你的项目制定测试策略和计划
  2. 建立代码审查流程和清单
  3. 配置CI/CD自动化测试
  4. 创建测试文档和知识库

🔗 相关链接

📢 Share this article