(六)Mocha+Chai测试框架

(六)Mocha+Chai测试框架

viEcho Lv5

概念

作为 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);
// --------合约的特定断言--------------
// 1.检查事件是否发出
await expect(contract.function())
.to.emit(contract, 'EventName')
.withArgs(arg1, arg2);
// 2.检查交易是否被回滚
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. 测试事件发射
    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; // 7天

    // 存入资金
    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); // 1 ETH = 100 tokens
    });

    it('应该允许用ETH购买代币', async function() {
    const ethAmount = ethers.utils.parseEther('1');
    const expectedTokens = 100;

    // 授权TokenSale合约可以转移代币
    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
// 定义fixture函数 setup
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() {
// 每次测试都调用setup获取全新环境
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);
// 特点
// 更简洁,适合单一合约的简单测试场景
// 支持参数化配置(如 supply 参数)
// 直接返回对象,调用时解构使用

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() {
// Hardhat 官方测试推荐写法
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();
// Hardhat 优化场景
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
it('测试用例', async () => {
const { token } = await loadFixture(deployTokenFixture);
// loadFixture 会自动缓存结果并回滚状态
});
//特点:
// 更符合 Hardhat 官方测试模板风格
// 函数名通常以 Fixture 结尾提高可读性
// 适合与 loadFixture 配合使用(Hardhat 的智能缓存 Fixtures 系统

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 表达"是什么"
describe('ERC20 Token', function() {
describe('transfer() 方法', function() {
// 测试转账功能本身
});
});
// 用 context 表达"在什么条件下"
describe('ERC20 Token', function() {
context('当发送方余额充足时', function() {
// 测试成功场景
});

context('当发送方余额不足时', function() {
// 测试失败场景
});
});
  • Title: (六)Mocha+Chai测试框架
  • Author: viEcho
  • Created at : 2025-07-27 16:11:39
  • Updated at : 2025-07-27 18:36:08
  • Link: https://viecho.github.io/2025/0727/mocha_chai.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments