03. 基本测试语法和写法
🎯 测试文件基本结构
1. 完整的测试文件模板
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Contract Name", function () {
// 声明变量
let contract;
let owner;
let user1;
let user2;
// 在所有测试前执行一次
before(async function () {
// 初始化操作,比如部署合约
});
// 每个测试前执行
beforeEach(async function () {
// 获取测试账户
[owner, user1, user2] = await ethers.getSigners();
// 部署合约
const ContractFactory = await ethers.getContractFactory("ContractName");
contract = await ContractFactory.deploy();
});
// 每个测试后执行
afterEach(async function () {
// 清理操作
});
// 所有测试后执行一次
after(async function () {
// 最终清理操作
});
// 测试套件
describe("Function Name", function () {
it("should work correctly", async function () {
// 测试用例
});
});
});
2. 简化的测试文件模板
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Contract Name", function () {
async function deployContract() {
const [owner, user1, user2] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("ContractName");
const contract = await Contract.deploy();
return { contract, owner, user1, user2 };
}
it("should work correctly", async function () {
const { contract, owner } = await deployContract();
// 测试逻辑
});
});
🧩 BDD 风格的三段式结构
BDD(Behavior Driven Development,行为驱动开发)的写作风格, Given:前置条件(测试场景的初始状态) 比如 “系统里已有 3 个 admin,cedric 还不是 admin” When:执行的动作(触发的行为) 比如 “调用 addAdmin(cedric.address)” Then:期望的结果(断言/验证) 比如 “现在有 4 个 admin,并且 cedric 已经是 admin 了”
context("Add admins", async function () {
it("Should allow adding an admin", async function () {
// Given
let admins = await Admin.getAllAdmins();
expect(admins.length).to.eq(3);
expect(await Admin.isAdmin(cedric.address)).to.be.false;
// When
await Admin.addAdmin(cedric.address);
// Then
admins = await Admin.getAllAdmins();
expect(admins.length).to.eq(4);
expect(await Admin.isAdmin(cedric.address)).to.be.true;
});
https://chatgpt.com/s/t_68df7bc88e0c81919266f2ab36c68883
合约初始化
const [owner, other] = await ethers.getSigners();
const CallContract = await ethers.getContractFactory('CallContract');
const callContract = await CallContract.deploy();
await callContract.waitForDeployment() // 等待部署完成
expect(await callContract.owner()).to.equal(owner.address);
beforeEach https://mochajs.org/next/features/hooks/#_top
在每个it模块开始时,都执行一遍
let xxx, xxx,xxx // 配合全局变量调用
beforeEach(async function(){
...
})
loadFixture
只执行一遍
const {loadFixture,} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const hardhatToken = await ethers.deployContract("Token");
return { hardhatToken, owner, addr1, addr2 };
}
describe("Token xxxx", function () {
it("Should xxxx", async function () {
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
....
});
})
测试基本写法
const { expect } = require("chai");
// time的导入路径
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
describe("TimeLock2", function(){
it("should workd", async function(){
let currnetTime = time.latest(); //最后一个区块的时间戳
// 设置下个交易的区块的时间,要有真实的修改状态才行,查询的话不生产新区块,所以看不到效果
await time.setNextBlockTimestamp(futureTime);
})
})
🔧 基本测试语法
1. describe - 测试套件
在mocha中 context() 是 describe()的别名,其他都一样,
// 描述合约
describe("Counter Contract", function () {
// 描述功能模块
describe("Increment Function", function () {
// 描述具体测试
it("should increment counter by 1", function () {});
});
describe("Decrement Function", function () {
it("should decrement counter by 1", function () {});
});
});
2. it - 测试用例
it("should increment counter", async function () {
// 测试逻辑
});
it("should fail when decrementing below zero", async function () {
// 测试逻辑
});
3. 钩子函数 (Hooks)
describe("Contract", function () {
before(async function () {
// 在所有测试前执行一次
console.log("Setting up test environment");
});
beforeEach(async function () {
// 每个测试前执行
this.contract = await deployContract();
});
afterEach(async function () {
// 每个测试后执行
// 清理操作
});
after(async function () {
// 所有测试后执行一次
console.log("Cleaning up test environment");
});
});
📊 断言语法 (Chai)
1. 基本断言
expect(await xxx).to.equal(..);
// 相等性断言
expect(value).to.equal(expectedValue);
expect(value).to.not.equal(unexpectedValue);
// 布尔值断言
expect(value).to.be.true;
expect(value).to.be.false;
// 存在性断言
expect(value).to.exist;
expect(value).to.not.exist;
// 类型断言
expect(value).to.be.a('string');
expect(value).to.be.an('object');
expect(value).to.be.instanceof(Contract);
2. 数值断言
// 大小比较
expect(value).to.be.greaterThan(100);
expect(value).to.be.lessThan(1000);
expect(value).to.be.at.least(100);
expect(value).to.be.at.most(1000);
// 范围断言
expect(value).to.be.within(100, 1000);
// 近似值断言
expect(value).to.be.closeTo(100, 1); // 100 ± 1
数字形式转换
// 人类可读 → Wei
ethers.parseUnits("100", 18) // "100000000000000000000"
// Wei → 人类可读
ethers.formatUnits("100000000000000000000", 18) // "100.0"
判断大金额相等
await payTable.receiveETH({value:10000000000000000n}); // 注意这里value没有引号,
expect(await payTable.getBalance()).to.equal(10000000000000000n); // 数字n表示BigNumber是ethers中的类型。V6版本的写法。
对于eth金额的操作
直接在传参的最后用大括号写{value:xxx}
await routerContract.addLiquidityETH(
tokenAddress, // token 地址
tokenLiquidityAmount, // token 数量
0, // amountTokenMin (滑点保护)
0, // amountETHMin (滑点保护)
signer.address, // 接收 LP token 的地址
deadline, // 截止时间
{ value: ethLiquidityAmount } // 🔥 关键:ETH 金额通过 value 传递
);
切换其他address
// 这样是对的,临时切换账号后立马执行操作
await Admin.connect(cedric).removeAdmin(deployer.address);
// 这样的话,其实第二句的时候,就又变回了默认的账号了
await Admin.connect(cedric);
await Admin.removeAdmin(deployer.address);
在合约中添加log
import "hardhat/console.sol";
console.log("Factory: pair address is %s", pair);
加载特定位置的合约
提供ABI,提供地址
const pair = await hre.ethers.getContractAt(
pairAbi, // pairAbi是pair对的abi 编译后得到的
await factory.getPair( // getPair是工厂合约中的函数,用于获取pair对的地址
await token1.getAddress(),
await token2.getAddress(),
signer
)
);
3. 字符串断言
// 包含断言
expect(string).to.include('substring');
expect(string).to.not.include('substring');
// 匹配断言
expect(string).to.match(/regex/);
// 长度断言
expect(string).to.have.lengthOf(10);
expect(string).to.have.length.above(5);
4. 数组断言
// 长度断言
expect(array).to.have.lengthOf(3);
expect(array).to.have.length.above(0);
// 包含断言
expect(array).to.include(element);
expect(array).to.not.include(element);
// 成员断言
expect(array).to.have.members([1, 2, 3]);
expect(array).to.contain.members([1, 2]);
更改时间
// 把 EVM 时间往后推一段时间
await ethers.provider.send("evm_increaseTime", [3600]);
// 然后挖一个新区块,让时间变动生效
await ethers.provider.send("evm_mine");
🚀 合约交互测试
1. 部署合约
it("should deploy successfully", async function () {
const Contract = await ethers.getContractFactory("Counter");
const contract = await Contract.deploy();
expect(contract.address).to.be.properAddress;
expect(await contract.count()).to.equal(0);
});
2. 调用合约函数
it("should increment counter", async function () {
const { contract } = await deployContract();
// 调用函数
await contract.increment();
// 验证结果
expect(await contract.count()).to.equal(1);
});
3. 测试函数返回值
it("should return correct value", async function () {
const { contract } = await deployContract();
// 获取返回值
const result = await contract.getValue();
// 验证返回值
expect(result).to.equal(42);
});
4. 测试状态变量
it("should update state variable", async function () {
const { contract } = await deployContract();
// 执行操作
await contract.setValue(100);
// 验证状态变量
expect(await contract.value()).to.equal(100);
});
🔐 权限和访问控制测试
1. 测试所有者权限
it("should only allow owner to pause", async function () {
const { contract, user1 } = await deployContract();
// 非所有者调用应该失败
await expect(
contract.connect(user1).pause()
).to.be.revertedWith("Ownable: caller is not the owner");
});
2. 测试角色权限
it("should only allow admin to mint", async function () {
const { contract, user1 } = await deployContract();
await expect(
contract.connect(user1).mint(user1.address, 100)
).to.be.revertedWith("AccessControl: account");
});
3. 测试修饰符
it("should require valid signature", async function () {
const { contract, user1 } = await deployContract();
await expect(
contract.connect(user1).executeWithSignature(
data,
invalidSignature
)
).to.be.revertedWith("Invalid signature");
});
⚠️ 错误和异常测试
1. 测试 revert
it("should revert with custom error", async function () {
const { contract } = await deployContract();
await expect(
contract.withdraw(1000)
).to.be.revertedWith("Insufficient balance");
});
2. 测试自定义错误
it("should revert with custom error", async function () {
const { contract } = await deployContract();
await expect(
contract.transfer(address(0), 100)
).to.be.revertedWithCustomError(contract, "InvalidAddress");
});
4. 测试不 revert
it("should not revert on valid input", async function () {
const { contract } = await deployContract();
await expect(
contract.transfer(user1.address, 100)
).to.not.be.reverted;
});
测试报出指定错误
await expect(....)to.be.reverted; // 会抛出错误
await expect(执行语句).to.be.revertedWith("too late"); // 报出指定错误
await expect(payTable.connect(other).getBalance())
.to.be.revertedWithCustomError(payTable, "OnlyOwnerCanCall") // 抛出自定义错误 revertedWithCustomError
.withArgs(other, "don`t allow"); // 自定义错误的参数 withArgs()
对于错误的抛出,为什么总是把await写到expect外面
it("should revert with 'not allow' when called by non-clock", async function () {
// 先明白called.connect(other).add(22,33))是返回的一个异步对象,如果用await等待这个异步对象了,那么
// 就会拿到最终异步执行的结果。
// 这里是要用chai断言库去拦截promise抛出的异常,所以不能拿到结果,要用还未执行异步promise传给expect
// await expect(await called.connect(other).add(22,33)).to.be.revertedWith("not allow"); // 错误
await expect(called.connect(other).add(22, 33)).to.be.revertedWith("not allow"); // 正确
})
3. 测试 panic
it("should panic on division by zero", async function () {
const { contract } = await deployContract();
await expect(
contract.divide(100, 0)
).to.be.revertedWithPanic(0x12); // Arithmetic overflow
});
📡 事件测试
1. 测试事件触发
it("should emit Transfer event", async function () {
const { contract, user1 } = await deployContract();
await expect(contract.transfer(user1.address, 100))
.to.emit(contract, "Transfer")
.withArgs(owner.address, user1.address, 100);
});
2. 测试事件参数
it("should emit event with correct parameters", async function () {
const { contract, user1 } = await deployContract();
const tx = await contract.transfer(user1.address, 100);
const receipt = await tx.wait();
// 获取事件
const event = receipt.events.find(e => e.event === "Transfer");
// 验证事件参数
expect(event.args.from).to.equal(owner.address);
expect(event.args.to).to.equal(user1.address);
expect(event.args.value).to.equal(100);
});
3. 测试多个事件
it("should emit multiple events", async function () {
const { contract, user1 } = await deployContract();
await expect(contract.batchTransfer([user1.address], [100]))
.to.emit(contract, "Transfer")
.and.to.emit(contract, "BatchTransferComplete");
});
🔢 数值和类型测试
1. BigInt 测试
it("should handle large numbers", async function () {
const { contract } = await deployContract();
const largeAmount = ethers.parseEther("1000000"); // 100万 ETH
await contract.setAmount(largeAmount);
const result = await contract.getAmount();
expect(result).to.equal(largeAmount);
});
2. 地址测试
it("should validate addresses", async function () {
const { contract } = await deployContract();
// 测试有效地址
expect(contract.address).to.be.properAddress;
// 测试零地址
expect(ethers.ZeroAddress).to.equal("0x0000000000000000000000000000000000000000");
});
3. 字节测试
it("should handle bytes correctly", async function () {
const { contract } = await deployContract();
const data = ethers.toUtf8Bytes("Hello World");
await contract.setData(data);
const result = await contract.getData();
expect(result).to.equal(data);
});
📋 数组和映射测试
1. 测试数组长度
it("should return correct array length", async function () {
const { contract } = await deployContract();
// 添加元素
await contract.addItem("item1");
await contract.addItem("item2");
// 验证长度
expect(await contract.getItemCount()).to.equal(2);
expect(await contract.getItems()).to.have.lengthOf(2);
});
2. 测试数组元素
it("should return correct array elements", async function () {
const { contract } = await deployContract();
await contract.addItem("item1");
await contract.addItem("item2");
const items = await contract.getItems();
expect(items[0]).to.equal("item1");
expect(items[1]).to.equal("item2");
});
3. 测试映射
it("should store and retrieve mapping values", async function () {
const { contract, user1 } = await deployContract();
// 设置映射值
await contract.setUserData(user1.address, "John Doe", 25);
// 获取映射值
const userData = await contract.getUserData(user1.address);
expect(userData.name).to.equal("John Doe");
expect(userData.age).to.equal(25);
});
🔄 状态变化测试
1. 测试状态转换
it("should change state correctly", async function () {
const { contract } = await deployContract();
// 初始状态
expect(await contract.state()).to.equal(0); // Pending
// 改变状态
await contract.approve();
expect(await contract.state()).to.equal(1); // Approved
await contract.reject();
expect(await contract.state()).to.equal(2); // Rejected
});
2. 测试余额变化
it("should update balances correctly", async function () {
const { contract, user1 } = await deployContract();
const initialBalance = await contract.balanceOf(owner.address);
const transferAmount = 100;
// 转账前
expect(await contract.balanceOf(owner.address)).to.equal(initialBalance);
expect(await contract.balanceOf(user1.address)).to.equal(0);
// 转账
await contract.transfer(user1.address, transferAmount);
// 转账后
expect(await contract.balanceOf(owner.address)).to.equal(initialBalance - transferAmount);
expect(await contract.balanceOf(user1.address)).to.equal(transferAmount);
});
🧪 边界条件测试
1. 零值测试
it("should handle zero values", async function () {
const { contract } = await deployContract();
// 测试零转账
await expect(contract.transfer(user1.address, 0)).to.not.be.reverted;
// 测试零地址
await expect(contract.transfer(ethers.ZeroAddress, 100)).to.be.reverted;
});
2. 最大值测试
it("should handle maximum values", async function () {
const { contract } = await deployContract();
const maxUint256 = ethers.MaxUint256;
await expect(contract.setAmount(maxUint256)).to.not.be.reverted;
expect(await contract.getAmount()).to.equal(maxUint256);
});
3. 边界值测试
it("should handle boundary values", async function () {
const { contract } = await deployContract();
const maxSupply = await contract.MAX_SUPPLY();
// 测试最大供应量
await expect(contract.mint(owner.address, maxSupply)).to.not.be.reverted;
// 测试超过最大供应量
await expect(contract.mint(owner.address, 1)).to.be.reverted;
});
📝 测试用例组织技巧
1. 按功能分组
describe("Counter Contract", function () {
describe("Basic Operations", function () {
it("should increment", function () {});
it("should decrement", function () {});
it("should reset", function () {});
});
describe("Edge Cases", function () {
it("should handle zero", function () {});
it("should handle maximum value", function () {});
});
describe("Access Control", function () {
it("should only allow owner to reset", function () {});
});
});
2. 使用共享变量
describe("Contract", function () {
let contract, owner, user1;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("ContractName");
contract = await Contract.deploy();
});
// 所有测试都可以使用 contract, owner, user1
});
3. 使用 this 上下文
describe("Contract", function () {
beforeEach(async function () {
this.contract = await deployContract();
this.owner = await ethers.getSigner(0);
this.user1 = await ethers.getSigner(1);
});
it("should work", async function () {
// 使用 this.contract, this.owner, this.user1
});
});
📚 总结
本章我们学习了:
- 测试文件的基本结构和组织方式
- 各种断言语法和用法
- 如何测试合约的不同功能
- 如何测试错误和异常情况
- 如何测试事件和状态变化
- 如何组织和管理测试用例
在下一章中,我们将深入学习如何测试合约的具体功能,包括更复杂的测试场景。