概念 作为 Web3 开发者,掌握 Mocha 和 Chai 是进行智能合约测试的基础;Mocha 是一个功能丰富的 JavaScript 测试框架,用于异步测试。Chai 提供了多种断言风格,最常用的是 expect 和 should。
测试的基本结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 describe ('合约名称' , function ( ) { before (function ( ) { }); beforeEach (function ( ) { }); after (function ( ) { }); afterEach (function ( ) { }); it ('应该做某件事' , function ( ) { }); }); const { expect } = require ('chai' );const { ethers } = require ('hardhat' );describe ('MyToken' , function ( ) { let MyToken ; let myToken; let owner; let addr1; let addr2; before (async function ( ) { [owner, addr1, addr2] = await ethers.getSigners (); MyToken = await ethers.getContractFactory ('MyToken' ); }); beforeEach (async function ( ) { myToken = await MyToken .deploy (1000000 ); await myToken.deployed (); }); it ('应该正确设置总供应量' , async function ( ) { const totalSupply = await myToken.totalSupply (); expect (totalSupply).to .equal (1000000 ); }); });
chai相关的断言方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 expect (value).to .equal (100 );expect (value).to .not .equal (200 );expect (value).to .be .true ;expect (value).to .be .false ;expect (value).to .be .null ;expect (value).to .be .undefined ;expect (value).to .exist ;expect (value).to .not .exist ;expect ([1 , 2 , 3 ]).to .include (2 );expect ('foobar' ).to .include ('foo' );expect ([1 , 2 , 3 ]).to .have .lengthOf (3 );await expect (contract.function ( )) .to .emit (contract, 'EventName' ) .withArgs (arg1, arg2); await expect (contract.function ( )).to .be .reverted ;await expect (contract.function ( )).to .be .revertedWith ('Error message' );
断言示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 describe ('MyToken 转账' , function ( ) { it ('应该正确转账' , async function ( ) { const initialOwnerBalance = await myToken.balanceOf (owner.address ); expect (initialOwnerBalance).to .equal (1000000 ); const transferAmount = 100 ; await myToken.transfer (addr1.address , transferAmount); const ownerBalance = await myToken.balanceOf (owner.address ); expect (ownerBalance).to .equal (1000000 - transferAmount); const addr1Balance = await myToken.balanceOf (addr1.address ); expect (addr1Balance).to .equal (transferAmount); }); it ('应该拒绝超额转账' , async function ( ) { const initialBalance = await myToken.balanceOf (owner.address ); await expect ( myToken.transfer (addr1.address , initialBalance + 1 ) ).to .be .revertedWith ('ERC20: transfer amount exceeds balance' ); }); });
特定场景下的合约代码
测试事件发射1 2 3 4 5 it ('应该在转账时发射 Transfer 事件' , async function ( ) { await expect (myToken.transfer (addr1.address , 100 )) .to .emit (myToken, 'Transfer' ) .withArgs (owner.address , addr1.address , 100 ); });
2.测试合约部署1 2 3 4 5 6 7 8 9 10 11 12 13 14 describe ('合约部署' , function ( ) { it ('应该正确设置合约所有者' , async function ( ) { const contractOwner = await myToken.owner (); expect (contractOwner).to .equal (owner.address ); }); it ('应该正确初始化名称和符号' , async function ( ) { const name = await myToken.name (); const symbol = await myToken.symbol (); expect (name).to .equal ('MyToken' ); expect (symbol).to .equal ('MTK' ); }); });
3.测试权限控制1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 describe ('权限控制' , function ( ) { it ('应该允许所有者铸造新代币' , async function ( ) { const mintAmount = 1000 ; await myToken.mint (owner.address , mintAmount); const balance = await myToken.balanceOf (owner.address ); expect (balance).to .equal (1000000 + mintAmount); }); it ('应该拒绝非所有者铸造代币' , async function ( ) { await expect ( myToken.connect (addr1).mint (addr1.address , 1000 ) ).to .be .revertedWith ('Ownable: caller is not the owner' ); }); });
4.时间相关测试1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 describe ('时间锁' , function ( ) { it ('应该允许在锁定期后提取资金' , async function ( ) { const lockPeriod = 7 * 24 * 60 * 60 ; await myToken.lockFunds (addr1.address , 1000 , lockPeriod); await expect (myToken.withdraw (addr1.address )) .to .be .revertedWith ('Funds are still locked' ); await ethers.provider .send ('evm_increaseTime' , [lockPeriod + 1 ]); await ethers.provider .send ('evm_mine' ); await expect (myToken.withdraw (addr1.address )) .to .emit (myToken, 'Withdrawal' ) .withArgs (addr1.address , 1000 ); }); });
5.测试合约交互1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 describe ('合约间交互' , function ( ) { let MyToken ; let TokenSale ; let myToken; let tokenSale; beforeEach (async function ( ) { [owner, buyer] = await ethers.getSigners (); MyToken = await ethers.getContractFactory ('MyToken' ); TokenSale = await ethers.getContractFactory ('TokenSale' ); myToken = await MyToken .deploy (1000000 ); tokenSale = await TokenSale .deploy (myToken.address , 100 ); }); it ('应该允许用ETH购买代币' , async function ( ) { const ethAmount = ethers.utils .parseEther ('1' ); const expectedTokens = 100 ; await myToken.approve (tokenSale.address , expectedTokens); await expect ( tokenSale.connect (buyer).buyTokens (expectedTokens, { value : ethAmount }) ) .to .emit (myToken, 'Transfer' ) .withArgs (owner.address , buyer.address , expectedTokens); }); });
测试块嵌套 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 describe ('MyToken' , function ( ) { describe ('转账功能' , function ( ) { it ('应该正常转账' , function ( ) { }); it ('应该拒绝超额转账' , function ( ) { }); }); describe ('授权功能' , function ( ) { context ('当授权时' , function ( ) { it ('应该允许被授权人转账' , function ( ) { }); it ('应该允许更新授权额度' , function ( ) { }); }); context ('当撤销授权时' , function ( ) { it ('应该拒绝被授权人转账' , function ( ) { }); }); }); });
使用fixture测试夹具 Fixtures(测试夹具)是一种在测试中重用初始化代码的模式,特别适合智能合约测试中需要重复部署合约或设置相同测试环境的场景 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const setup = async ( ) => { const [owner, addr1, addr2] = await ethers.getSigners (); const MyToken = await ethers.getContractFactory ('MyToken' ); const myToken = await MyToken .deploy (1000000 ); return { myToken, owner, addr1, addr2 }; }; describe ('MyToken' , function ( ) { it ('测试用例1' , async function ( ) { const { myToken, owner } = await setup (); const balance = await myToken.balanceOf (owner.address ); expect (balance).to .equal (1000000 ); }); it ('测试用例2' , async function ( ) { const { myToken, addr1 } = await setup (); await myToken.transfer (addr1.address , 100 ); expect (await myToken.balanceOf (addr1.address )).to .equal (100 ); }); });
fixtures函数对比 1.箭头函数
1 2 3 4 5 6 7 8 9 10 11 12 13 const setupWithOptions = async (supply = 1000000 ) => { const [owner] = await ethers.getSigners (); const MyToken = await ethers.getContractFactory ('MyToken' ); const myToken = await MyToken .deploy (supply); return { myToken, owner }; }; const { myToken, owner } = await setupWithOptions (500000 );
2.传统写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 async function deployTokenFixture ( ) { const [owner, addr1] = await ethers.getSigners (); const Token = await ethers.getContractFactory ('Token' ); const token = await Token .deploy (); return { token, owner, addr1 }; } const { token, owner } = await deployTokenFixture ();const { loadFixture } = require ('@nomicfoundation/hardhat-network-helpers' );it ('测试用例' , async () => { const { token } = await loadFixture (deployTokenFixture); });
loadFixture介绍 1.自动缓存 fixture 的执行结果 2.状态回滚 在每个测试完成后 3.性能优化 避免重复部署合约
特性
普通 Fixture
loadFixture
执行次数
每次测试都执行
只执行一次,后续复用
状态隔离
需要手动处理
自动回滚
性能
较慢(重复部署)
(避免重复部署)
适用场景
简单测试
复杂测试套件
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const { loadFixture } = require ("@nomicfoundation/hardhat-network-helpers" );const { expect } = require ("chai" );describe ("ERC20 Token" , function ( ) { async function deployTokenFixture ( ) { const [owner, user1, user2] = await ethers.getSigners (); const Token = await ethers.getContractFactory ("MyToken" ); const token = await Token .deploy (1000000 ); return { token, owner, user1, user2 }; } it ("should assign total supply to owner" , async function ( ) { const { token, owner } = await loadFixture (deployTokenFixture); expect (await token.balanceOf (owner.address )).to .equal (1000000 ); }); it ("should transfer tokens between accounts" , async function ( ) { const { token, owner, user1 } = await loadFixture (deployTokenFixture); await token.transfer (user1.address , 100 ); expect (await token.balanceOf (user1.address )).to .equal (100 ); }); it ("should fail if sender doesn't have enough tokens" , async function ( ) { const { token, owner, user1, user2 } = await loadFixture (deployTokenFixture); await expect ( token.connect (user1).transfer (user2.address , 1 ) ).to .be .revertedWith ("ERC20: transfer amount exceeds balance" ); }); });
【注意事项】 1.不要在 fixture 中执行测试逻辑,只做初始化 2.避免在 fixture 中使用动态参数(除非包裹在函数中) 3.适合于大多数合约测试场景,但极简单测试可能不需要 4.记住它只优化了部署开销,测试中的交易仍会执行
describe和context的对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 describe ('ERC20 Token' , function ( ) { describe ('transfer() 方法' , function ( ) { }); }); describe ('ERC20 Token' , function ( ) { context ('当发送方余额充足时' , function ( ) { }); context ('当发送方余额不足时' , function ( ) { }); });