跳到主要内容

05. 高级测试技巧

🎯 本章目标

本章将介绍智能合约测试的高级技巧,包括:

  • 测试优化和性能提升
  • 模拟外部依赖和预言机
  • 时间相关的测试
  • 压力测试和边界测试
  • 测试数据管理
  • 测试覆盖率优化

⚡ 测试优化和性能提升

1. 使用 loadFixture 优化测试

loadFixture 可以显著提高测试性能,避免重复部署合约:

const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

describe("Optimized Tests", function () {
async function deployContractFixture() {
const [owner, user1, user2] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("MyContract");
const contract = await Contract.deploy();

return { contract, owner, user1, user2 };
}

it("should work correctly", async function () {
const { contract, owner } = await loadFixture(deployContractFixture);
// 测试逻辑
});

it("should handle edge cases", async function () {
const { contract, user1 } = await loadFixture(deployContractFixture);
// 测试逻辑
});
});

优势:

  • 避免重复部署合约
  • 减少测试执行时间
  • 保持测试独立性

2. 批量测试优化

对于相似的测试,可以使用循环来减少代码重复:

describe("Batch Testing", function () {
const testCases = [
{ input: 1, expected: 2 },
{ input: 5, expected: 10 },
{ input: 10, expected: 20 },
{ input: 100, expected: 200 }
];

testCases.forEach(({ input, expected }) => {
it(`should double ${input} to ${expected}`, async function () {
const { contract } = await loadFixture(deployContractFixture);
const result = await contract.double(input);
expect(result).to.equal(expected);
});
});
});

3. 并行测试优化

使用 parallel: true 配置来并行运行测试:

// hardhat.config.js
module.exports = {
mocha: {
parallel: true,
jobs: 4, // 并行作业数量
},
};

注意事项:

  • 确保测试之间没有状态依赖
  • 避免使用共享的全局变量
  • 每个测试都应该独立运行

🎭 模拟外部依赖和预言机

1. 模拟价格预言机

describe("Price Oracle Mock", function () {
let contract, mockOracle, owner, user1;

beforeEach(async function () {
[owner, user1] = await ethers.getSigners();

// 部署模拟预言机
const MockOracle = await ethers.getContractFactory("MockPriceOracle");
mockOracle = await MockOracle.deploy();

// 部署依赖预言机的合约
const Contract = await ethers.getContractFactory("PriceDependentContract");
contract = await Contract.deploy(mockOracle.address);
});

it("should use oracle price for calculations", async function () {
// 设置预言机价格
await mockOracle.setPrice(ethers.parseEther("1000")); // $1000

const result = await contract.calculateValue(ethers.parseEther("1"));
expect(result).to.equal(ethers.parseEther("1000"));
});

it("should handle oracle price changes", async function () {
// 初始价格
await mockOracle.setPrice(ethers.parseEther("1000"));
let result = await contract.calculateValue(ethers.parseEther("1"));
expect(result).to.equal(ethers.parseEther("1000"));

// 价格变化
await mockOracle.setPrice(ethers.parseEther("2000"));
result = await contract.calculateValue(ethers.parseEther("1"));
expect(result).to.equal(ethers.parseEther("2000"));
});

it("should handle oracle failures", async function () {
// 模拟预言机失败
await mockOracle.setShouldRevert(true);

await expect(
contract.calculateValue(ethers.parseEther("1"))
).to.be.revertedWith("Oracle call failed");
});
});

2. 模拟外部合约

describe("External Contract Mock", function () {
let contract, mockToken, mockVault, owner;

beforeEach(async function () {
[owner] = await ethers.getSigners();

// 部署模拟代币
const MockToken = await ethers.getContractFactory("MockERC20");
mockToken = await MockToken.deploy("Mock Token", "MTK");

// 部署模拟金库
const MockVault = await ethers.getContractFactory("MockVault");
mockVault = await MockVault.deploy(mockToken.address);

// 部署主合约
const Contract = await ethers.getContractFactory("MainContract");
contract = await Contract.deploy(mockToken.address, mockVault.address);
});

it("should interact with external contracts correctly", async function () {
// 给用户分配代币
await mockToken.mint(owner.address, ethers.parseEther("1000"));

// 授权合约使用代币
await mockToken.approve(contract.address, ethers.parseEther("1000"));

// 执行操作
await contract.depositToVault(ethers.parseEther("500"));

// 验证外部合约状态
expect(await mockVault.getDeposit(owner.address))
.to.equal(ethers.parseEther("500"));
});
});

3. 模拟随机数生成器

describe("Random Number Generator Mock", function () {
let contract, mockRNG, owner;

beforeEach(async function () {
[owner] = await ethers.getSigners();

const MockRNG = await ethers.getContractFactory("MockRandomNumberGenerator");
mockRNG = await MockRNG.deploy();

const Contract = await ethers.getContractFactory("RandomContract");
contract = await Contract.deploy(mockRNG.address);
});

it("should generate predictable random numbers", async function () {
// 设置固定的随机数
await mockRNG.setRandomNumber(42);

const result = await contract.generateRandom();
expect(result).to.equal(42);
});

it("should handle multiple random numbers", async function () {
const randomNumbers = [1, 5, 10, 15, 20];

for (let i = 0; i < randomNumbers.length; i++) {
await mockRNG.setRandomNumber(randomNumbers[i]);
const result = await contract.generateRandom();
expect(result).to.equal(randomNumbers[i]);
}
});
});

⏰ 时间相关的测试

1. 时间操作基础

describe("Time-based Tests", function () {
let contract, owner;

beforeEach(async function () {
[owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("TimeBasedContract");
contract = await Contract.deploy();
});

it("should handle time increases", async function () {
const initialTime = await contract.getCurrentTime();

// 增加时间
await ethers.provider.send("evm_increaseTime", [3600]); // 增加1小时
await ethers.provider.send("evm_mine");

const newTime = await contract.getCurrentTime();
expect(newTime).to.be.greaterThan(initialTime);
});

it("should handle specific timestamps", async function () {
const targetTime = Math.floor(Date.now() / 1000) + 3600; // 1小时后

await ethers.provider.send("evm_setNextBlockTimestamp", [targetTime]);
await ethers.provider.send("evm_mine");

const currentTime = await contract.getCurrentTime();
expect(currentTime).to.equal(targetTime);
});

it("should handle time-based functions", async function () {
// 设置初始时间
const startTime = Math.floor(Date.now() / 1000);
await ethers.provider.send("evm_setNextBlockTimestamp", [startTime]);
await ethers.provider.send("evm_mine");

// 启动计时器
await contract.startTimer();

// 等待一段时间
await ethers.provider.send("evm_increaseTime", [86400]); // 1天
await ethers.provider.send("evm_mine");

// 检查计时器状态
expect(await contract.isTimerExpired()).to.be.true;
});
});

2. 时间锁定测试

describe("Time Lock Tests", function () {
let contract, owner, user1;

beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("TimeLockContract");
contract = await Contract.deploy(3600); // 1小时锁定时间
});

it("should prevent withdrawal before lock period", async function () {
await contract.connect(user1).deposit({ value: ethers.parseEther("1") });

// 尝试立即取款
await expect(
contract.connect(user1).withdraw()
).to.be.revertedWith("Lock period not expired");
});

it("should allow withdrawal after lock period", async function () {
await contract.connect(user1).deposit({ value: ethers.parseEther("1") });

// 等待锁定时间
await ethers.provider.send("evm_increaseTime", [3600]);
await ethers.provider.send("evm_mine");

// 现在应该可以取款
await expect(contract.connect(user1).withdraw()).to.not.be.reverted;
});

it("should handle multiple deposits with different lock times", async function () {
// 第一次存款
await contract.connect(user1).deposit({ value: ethers.parseEther("1") });

// 等待一段时间
await ethers.provider.send("evm_increaseTime", [1800]); // 30分钟
await ethers.provider.send("evm_mine");

// 第二次存款
await contract.connect(user1).deposit({ value: ethers.parseEther("1") });

// 再等待30分钟
await ethers.provider.send("evm_increaseTime", [1800]);
await ethers.provider.send("evm_mine");

// 第一次存款应该可以取款,第二次不行
expect(await contract.canWithdraw(user1.address, 0)).to.be.true;
expect(await contract.canWithdraw(user1.address, 1)).to.be.false;
});
});

🧪 压力测试和边界测试

1. 大量数据测试

describe("Stress Testing", function () {
let contract, owner;

beforeEach(async function () {
[owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("StressTestContract");
contract = await Contract.deploy();
});

it("should handle large number of users", async function () {
const userCount = 1000;

// 批量创建用户
for (let i = 0; i < userCount; i++) {
const [user] = await ethers.getSigners(i + 1);
await contract.addUser(user.address, `User${i}`);
}

expect(await contract.getUserCount()).to.equal(userCount);

// 验证随机用户
const randomIndex = Math.floor(Math.random() * userCount);
const user = await contract.getUser(randomIndex);
expect(user.name).to.equal(`User${randomIndex}`);
});

it("should handle large data structures", async function () {
const dataSize = 10000;

// 添加大量数据
for (let i = 0; i < dataSize; i++) {
await contract.addData(`Data${i}`, i);
}

expect(await contract.getDataCount()).to.equal(dataSize);

// 验证数据完整性
for (let i = 0; i < Math.min(100, dataSize); i++) {
const data = await contract.getData(i);
expect(data.name).to.equal(`Data${i}`);
expect(data.value).to.equal(i);
}
});

it("should handle concurrent operations", async function () {
const operationCount = 100;
const promises = [];

// 并发执行操作
for (let i = 0; i < operationCount; i++) {
promises.push(contract.addData(`Data${i}`, i));
}

// 等待所有操作完成
await Promise.all(promises);

expect(await contract.getDataCount()).to.equal(operationCount);
});
});

2. 边界值测试

describe("Boundary Value Testing", function () {
let contract, owner;

beforeEach(async function () {
[owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("BoundaryTestContract");
contract = await Contract.deploy();
});

it("should handle minimum values", async function () {
// 测试最小值
await expect(contract.setValue(0)).to.not.be.reverted;
expect(await contract.getValue()).to.equal(0);

// 测试最小地址
await expect(contract.setAddress(ethers.ZeroAddress)).to.not.be.reverted;
expect(await contract.getAddress()).to.equal(ethers.ZeroAddress);
});

it("should handle maximum values", async function () {
// 测试最大 uint256
const maxUint256 = ethers.MaxUint256;
await expect(contract.setValue(maxUint256)).to.not.be.reverted;
expect(await contract.getValue()).to.equal(maxUint256);

// 测试最大地址
const maxAddress = "0xffffffffffffffffffffffffffffffffffffffff";
await expect(contract.setAddress(maxAddress)).to.not.be.reverted;
expect(await contract.getAddress()).to.equal(maxAddress);
});

it("should handle edge cases", async function () {
// 测试边界附近的值
const edgeValues = [
1, // 最小值 + 1
255, // uint8 最大值
65535, // uint16 最大值
4294967295, // uint32 最大值
ethers.MaxUint256 - 1n // uint256 最大值 - 1
];

for (const value of edgeValues) {
await expect(contract.setValue(value)).to.not.be.reverted;
expect(await contract.getValue()).to.equal(value);
}
});

it("should handle overflow and underflow", async function () {
// 测试溢出
await contract.setValue(ethers.MaxUint256);

await expect(
contract.increment()
).to.be.revertedWith("Arithmetic overflow");

// 测试下溢
await contract.setValue(0);

await expect(
contract.decrement()
).to.be.revertedWith("Arithmetic underflow");
});
});

📊 测试数据管理

1. 测试数据工厂

// test/factories/UserFactory.js
class UserFactory {
static createUser(index = 0) {
return {
name: `User${index}`,
email: `user${index}@example.com`,
age: 20 + (index % 50),
address: `0x${index.toString(16).padStart(40, '0')}`
};
}

static createUsers(count) {
return Array.from({ length: count }, (_, i) => this.createUser(i));
}

static createUserWithCustomData(customData) {
return { ...this.createUser(), ...customData };
}
}

module.exports = UserFactory;

2. 测试数据使用

const UserFactory = require("../factories/UserFactory");

describe("Data Management Tests", function () {
let contract, owner;

beforeEach(async function () {
[owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("DataContract");
contract = await Contract.deploy();
});

it("should handle single user data", async function () {
const userData = UserFactory.createUser(1);

await contract.addUser(
userData.name,
userData.email,
userData.age,
userData.address
);

const user = await contract.getUser(0);
expect(user.name).to.equal(userData.name);
expect(user.email).to.equal(userData.email);
expect(user.age).to.equal(userData.age);
expect(user.addr).to.equal(userData.address);
});

it("should handle multiple users", async function () {
const users = UserFactory.createUsers(10);

for (const userData of users) {
await contract.addUser(
userData.name,
userData.email,
userData.age,
userData.address
);
}

expect(await contract.getUserCount()).to.equal(10);
});

it("should handle custom user data", async function () {
const customUser = UserFactory.createUserWithCustomData({
name: "Custom User",
age: 99
});

await contract.addUser(
customUser.name,
customUser.email,
customUser.age,
customUser.address
);

const user = await contract.getUser(0);
expect(user.name).to.equal("Custom User");
expect(user.age).to.equal(99);
});
});

3. 测试数据清理

describe("Data Cleanup Tests", function () {
let contract, owner;

beforeEach(async function () {
[owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("DataContract");
contract = await Contract.deploy();
});

afterEach(async function () {
// 清理测试数据
await contract.cleanup();
});

it("should clean up after each test", async function () {
// 添加一些测试数据
await contract.addUser("Test User", "test@example.com", 25, owner.address);

// 验证数据存在
expect(await contract.getUserCount()).to.equal(1);

// afterEach 会自动清理
});

it("should start with clean state", async function () {
// 这个测试应该从干净的状态开始
expect(await contract.getUserCount()).to.equal(0);
});
});

📈 测试覆盖率优化

1. 覆盖率配置

// hardhat.config.js
module.exports = {
solidity: "0.8.19",
networks: {
hardhat: {
chainId: 31337,
},
},
mocha: {
timeout: 40000,
},
// 覆盖率配置
coverage: {
enabled: true,
runs: 200,
exclude: [
"contracts/mocks/",
"contracts/test/",
"contracts/examples/"
],
reporter: [
"text",
"lcov",
"html"
],
check: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
};

2. 覆盖率报告解读

# 运行覆盖率测试
npx hardhat coverage

覆盖率报告包含:

  • Statements: 语句覆盖率
  • Branches: 分支覆盖率
  • Functions: 函数覆盖率
  • Lines: 行覆盖率

3. 提高覆盖率的方法

describe("Coverage Improvement", function () {
let contract, owner, user1;

beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("CoverageContract");
contract = await Contract.deploy();
});

it("should cover all branches", async function () {
// 测试所有可能的分支

// 分支1: 正常情况
await contract.setValue(100);
expect(await contract.getValue()).to.equal(100);

// 分支2: 边界情况
await contract.setValue(0);
expect(await contract.getValue()).to.equal(0);

// 分支3: 最大值情况
await contract.setValue(ethers.MaxUint256);
expect(await contract.getValue()).to.equal(ethers.MaxUint256);

// 分支4: 错误情况
await expect(
contract.setValue(-1)
).to.be.revertedWith("Invalid value");
});

it("should cover all error conditions", async function () {
// 测试所有错误情况

// 权限错误
await expect(
contract.connect(user1).adminOnlyFunction()
).to.be.revertedWith("Not admin");

// 状态错误
await expect(
contract.functionThatRequiresState()
).to.be.revertedWith("Invalid state");

// 参数错误
await expect(
contract.functionWithValidation("")
).to.be.revertedWith("Invalid input");
});

it("should cover all events", async function () {
// 测试所有事件

await expect(contract.emitEvent1("data1"))
.to.emit(contract, "Event1")
.withArgs("data1");

await expect(contract.emitEvent2("data2", 42))
.to.emit(contract, "Event2")
.withArgs("data2", 42);

await expect(contract.emitMultipleEvents())
.to.emit(contract, "Event1")
.and.to.emit(contract, "Event2");
});
});

📚 总结

本章我们学习了:

  • 测试优化: 使用 loadFixture、批量测试、并行测试等
  • 模拟外部依赖: 预言机、外部合约、随机数生成器等
  • 时间相关测试: 时间操作、时间锁定等
  • 压力测试: 大量数据、并发操作、边界值等
  • 测试数据管理: 数据工厂、数据清理等
  • 覆盖率优化: 配置、报告解读、提高方法等

关键要点:

  1. 性能优化: 使用适当的工具和技术提高测试执行效率
  2. 外部依赖: 通过模拟来测试与外部系统的交互
  3. 时间处理: 正确测试时间相关的合约功能
  4. 边界测试: 全面测试各种边界条件和异常情况
  5. 数据管理: 有效管理测试数据,保持测试的独立性
  6. 覆盖率: 通过高覆盖率确保代码质量

在下一章中,我们将学习测试最佳实践,包括测试策略、代码质量等。


🧠 练习

  1. 为你的合约创建模拟预言机并测试价格相关功能
  2. 实现时间相关的测试用例
  3. 创建测试数据工厂来管理测试数据
  4. 运行覆盖率测试并分析结果

🔗 相关链接

📢 Share this article