文章目录
- [1. 为什么需要单元测试](#1. 为什么需要单元测试)
-
- [1.1 没有测试的问题](#1.1 没有测试的问题)
- [1.2 单元测试的优势](#1.2 单元测试的优势)
- [1.3 测试驱动开发(TDD)](#1.3 测试驱动开发(TDD))
- [2. Hardhat 3测试框架概览](#2. Hardhat 3测试框架概览)
-
- [2.1 核心组件](#2.1 核心组件)
- [2.2 Mocha测试运行器](#2.2 Mocha测试运行器)
- [2.3 Chai断言库](#2.3 Chai断言库)
- [2.4 Hardhat插件](#2.4 Hardhat插件)
- [3. 测试文件结构](#3. 测试文件结构)
-
- [3.1 文件组织](#3.1 文件组织)
- [3.2 测试文件基本结构](#3.2 测试文件基本结构)
- [3.3 测试组织结构](#3.3 测试组织结构)
- [3.4 Hardhat 3的特殊之处](#3.4 Hardhat 3的特殊之处)
- [4. Mocha测试结构详解](#4. Mocha测试结构详解)
-
- [4.1 钩子函数](#4.1 钩子函数)
- [4.2 钩子函数示例](#4.2 钩子函数示例)
- [4.3 describe和it](#4.3 describe和it)
- [4.4 异步测试](#4.4 异步测试)
- [5. Chai断言语法基础](#5. Chai断言语法基础)
-
- [5.1 基础断言](#5.1 基础断言)
- [5.2 链式断言](#5.2 链式断言)
- [5.3 数组断言](#5.3 数组断言)
- [5.4 对象断言](#5.4 对象断言)
- [5.5 字符串断言](#5.5 字符串断言)
- [5.6 BigInt处理](#5.6 BigInt处理)
- [6. Hardhat 3特定断言](#6. Hardhat 3特定断言)
-
- [6.1 事件断言](#6.1 事件断言)
- [6.2 余额变化断言](#6.2 余额变化断言)
- [6.3 回退断言](#6.3 回退断言)
- [7. 合约部署和函数测试](#7. 合约部署和函数测试)
-
- [7.1 部署测试](#7.1 部署测试)
- [7.2 函数测试](#7.2 函数测试)
- [7.3 边界测试](#7.3 边界测试)
- [7.4 测试编写原则](#7.4 测试编写原则)
- [8. 事件触发测试](#8. 事件触发测试)
-
- [8.1 基础事件测试](#8.1 基础事件测试)
- [8.2 多个事件测试](#8.2 多个事件测试)
- [8.3 查询历史事件](#8.3 查询历史事件)
- [8.4 事件测试注意事项](#8.4 事件测试注意事项)
- [9. 错误和回退测试](#9. 错误和回退测试)
-
- [9.1 require回退测试](#9.1 require回退测试)
- [9.2 自定义错误测试](#9.2 自定义错误测试)
- [9.3 Panic错误测试](#9.3 Panic错误测试)
- [9.4 错误测试最佳实践](#9.4 错误测试最佳实践)
- [10. 时间和区块操作](#10. 时间和区块操作)
-
- [10.1 时间操作](#10.1 时间操作)
- [10.2 区块操作](#10.2 区块操作)
- [10.3 时间锁合约测试示例](#10.3 时间锁合约测试示例)
- [10.4 注意事项](#10.4 注意事项)
- [11. 快照和恢复](#11. 快照和恢复)
-
- [11.1 loadFixture概述](#11.1 loadFixture概述)
- [11.2 Fixture函数定义](#11.2 Fixture函数定义)
- [11.3 使用loadFixture](#11.3 使用loadFixture)
- [11.4 复杂Fixture示例](#11.4 复杂Fixture示例)
- [11.5 loadFixture的优势](#11.5 loadFixture的优势)
- [12. 测试覆盖率](#12. 测试覆盖率)
-
- [12.1 覆盖率概述](#12.1 覆盖率概述)
- [12.2 配置覆盖率](#12.2 配置覆盖率)
- [12.3 覆盖率报告](#12.3 覆盖率报告)
- [12.4 覆盖率目标](#12.4 覆盖率目标)
- [12.5 提高覆盖率](#12.5 提高覆盖率)
- [13. Gas报告](#13. Gas报告)
-
- [13.1 Gas报告概述](#13.1 Gas报告概述)
- [13.2 生成Gas报告](#13.2 生成Gas报告)
- [13.3 Gas报告示例](#13.3 Gas报告示例)
- [13.4 Gas优化建议](#13.4 Gas优化建议)
- [14. 测试最佳实践](#14. 测试最佳实践)
-
- [14.1 命名和组织](#14.1 命名和组织)
- [14.2 测试独立性](#14.2 测试独立性)
- [14.3 全面覆盖](#14.3 全面覆盖)
- [14.4 错误处理](#14.4 错误处理)
- [14.5 性能优化](#14.5 性能优化)
- [14.6 代码复用](#14.6 代码复用)
- [14.7 文档化](#14.7 文档化)
- [15. 常见错误和解决方案](#15. 常见错误和解决方案)
-
- [15.1 忘记使用await](#15.1 忘记使用await)
- [15.2 事件名称错误](#15.2 事件名称错误)
- [15.3 参数类型不匹配](#15.3 参数类型不匹配)
- [15.4 错误消息不匹配](#15.4 错误消息不匹配)
- [15.5 网络连接问题](#15.5 网络连接问题)
- [15.6 合约未部署](#15.6 合约未部署)
- [16. 实战演示](#16. 实战演示)
-
- [16.1 完整测试示例](#16.1 完整测试示例)
- [16.2 运行测试](#16.2 运行测试)
- 17.官方参考文档
1. 为什么需要单元测试
1.1 没有测试的问题
在开始学习具体的测试技术之前,我们先来思考一个问题:为什么需要单元测试?
从没有测试的角度来看,我们会遇到很多问题:
-
手动测试耗时且容易遗漏:
- 每次修改代码后,都需要手动测试每个功能
- 这个过程非常耗时,而且容易遗漏边界情况
- 随着功能增加,手动测试的工作量呈指数级增长
- 在复杂的DeFi协议中,手动测试几乎不可能覆盖所有场景
-
重构缺乏信心:
- 当我们需要重构代码时,由于缺乏测试的保护,往往缺乏信心
- 担心会引入新的问题,导致不敢进行必要的代码优化
- 代码质量逐渐下降,技术债务不断积累
-
生产环境风险:
- 没有经过充分测试的代码,问题可能会在生产环境中暴露
- 智能合约一旦部署就无法修改,问题可能导致实际损失
- 历史上很多安全事件都是因为测试不充分导致的
-
团队协作困难:
- 没有测试的话,团队成员无法保证代码的质量
- 代码审查时难以验证功能是否正确
- 新成员接手项目时,缺乏理解代码行为的文档
1.2 单元测试的优势
相比之下,单元测试能够带来很多优势:
-
自动化验证功能:
- 测试可以自动化运行,大大节省时间
- 一次编写,多次运行
- 可以在每次代码变更后自动验证功能
-
快速反馈问题:
- 当代码出现问题时,测试能够快速反馈
- 帮助我们及时发现问题,而不是等到生产环境
- 减少调试时间,提高开发效率
-
测试即文档:
- 测试本身也是一种文档,能够清晰地描述合约的行为
- 通过阅读测试用例,可以快速理解合约的功能
- 比传统的文档更容易维护和更新
-
提升代码质量:
- 编写测试的过程,会促使我们思考各种边界情况
- 帮助我们发现设计上的问题
- 提高代码的健壮性和可靠性
-
支持持续集成:
- 测试可以集成到CI/CD流程中
- 自动化的质量检查
- 确保每次提交的代码都经过验证
-
安全重构:
- 有了测试的保护,我们可以放心地进行重构
- 测试会告诉我们重构是否破坏了现有功能
- 支持持续改进代码质量
-
降低维护成本:
- 虽然编写测试需要时间,但长期来看降低了维护成本
- 减少了生产环境的问题
- 提高了开发效率
1.3 测试驱动开发(TDD)
测试驱动开发(Test-Driven Development)是一种开发方法论,其核心思想是:
- 先写测试:在实现功能之前,先编写测试用例
- 运行测试:运行测试,确认测试失败(因为功能还没实现)
- 实现功能:编写最少的代码使测试通过
- 重构:在测试通过的基础上进行重构优化
TDD的优势:
- 确保代码满足需求
- 提高测试覆盖率
- 促进更好的设计
- 增强开发信心
虽然TDD不是必须的,但理解其思想有助于编写更好的测试。
2. Hardhat 3测试框架概览
2.1 核心组件
Hardhat 3的测试框架主要由三个核心组件组成:Mocha、Chai和Hardhat插件。这三个组件配合使用,构成了一个功能完善的测试框架。
组件架构:
bash
Hardhat 3 测试框架
├── Mocha(测试运行器)
│ ├── 测试结构组织
│ ├── 钩子函数
│ └── 异步支持
├── Chai(断言库)
│ ├── 基础断言
│ ├── 链式语法
│ └── BDD/TDD风格
└── Hardhat插件
├── hardhat-chai-matchers(区块链断言)
└── hardhat-network-helpers(网络工具)
2.2 Mocha测试运行器
Mocha是一个成熟的测试运行器,它是JavaScript生态系统中最流行的测试框架之一。
Mocha的核心特性:
-
测试结构组织:
- 提供describe和it这样的结构来组织测试
- 支持嵌套的describe创建子套件
- 清晰的测试层次结构
-
钩子函数:
- before:所有测试前执行一次
- beforeEach:每个测试前执行
- afterEach:每个测试后执行
- after:所有测试后执行一次
-
异步支持:
- 原生支持Promise
- 支持async/await语法
- 可以测试异步操作
-
丰富的报告:
- 多种报告格式
- 详细的错误信息
- 测试执行时间统计
Mocha的优势:
- 配置灵活,可以适应各种项目需求
- 报告丰富,提供详细的测试结果
- 社区非常成熟,有大量的文档和示例
- 与各种工具集成良好
2.3 Chai断言库
Chai是一个强大的断言库,它提供了多种风格的断言方式。
Chai的三种风格:
- expect风格(推荐):
bash
expect(value).to.equal(42);
expect(value).to.be.true;
- 可读性强
- 链式语法
- 适合BDD风格
- should风格:
bash
value.should.equal(42);
value.should.be.true;
- 更接近自然语言
- 需要扩展Object.prototype
- assert风格:
bash
assert.equal(value, 42);
assert.isTrue(value);
- 类似Node.js的assert模块
- 适合TDD风格
Chai的核心特性:
- 链式语法:让代码可读性非常强
- BDD和TDD风格:支持两种开发风格
- 插件丰富:有大量的插件扩展功能
- 文档完善:有详细的API文档
在Hardhat 3中,我们主要使用expect风格,因为它与Hardhat的区块链特定断言配合最好。
2.4 Hardhat插件
Hardhat插件为测试提供了区块链特定的功能,这些功能是普通测试框架无法提供的。
hardhat-chai-matchers:
这个插件扩展了Chai的断言能力,提供了专门用于区块链测试的断言方法:
-
事件断言:
- .to.emit():验证事件是否被触发
- .withArgs():验证事件参数
-
余额变化断言:
- .to.changeEtherBalance():验证ETH余额变化
- .to.changeEtherBalances():验证多个账户的余额变化
-
回退断言:
- .to.be.revertedWith():验证带有错误消息的回退
- .to.be.reverted:验证无原因的回退
- .to.be.revertedWithCustomError():验证自定义错误
- .to.be.revertedWithPanic():验证panic错误
hardhat-network-helpers:
这个插件提供了网络操作和快照恢复功能:
-
时间操作:
- time.increase():增加时间
- time.increaseTo():跳转到特定时间
- time.setNextBlockTimestamp():设置下一个区块的时间戳
-
区块操作:
- mine():挖掘指定数量的区块
- mineUpTo():挖掘到指定区块号
-
快照恢复:
- loadFixture():加载Fixture并创建快照
- 自动恢复状态,提升测试速度
插件安装:
在Hardhat 3中,这些插件已经包含在@nomicfoundation/hardhat-toolbox-mocha-ethers中,不需要单独安装。
3. 测试文件结构
3.1 文件组织
在Hardhat 3项目中,测试文件通常放在test目录下,命名规范是ContractName.test.ts,一个合约对应一个测试文件。
目录结构:
bash
project/
├── contracts/
│ └── Counter.sol
├── test/
│ ├── Counter.test.ts
│ └── Token.test.ts
└── hardhat.config.ts
命名规范:
- 测试文件以.test.ts结尾
- 文件名与合约名对应
- 使用PascalCase命名
3.2 测试文件基本结构
测试文件的基本结构包括导入语句、网络连接、以及测试套件的组织。
基本结构示例:
bash
// 导入语句
import { expect } from "chai";
import { network } from "hardhat";
// 连接网络(Hardhat 3新方式)
const { ethers, networkHelpers } = await network.connect();
// 定义Fixture函数
async function deployCounterFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const counter = await ethers.deployContract("Counter");
return { counter, owner, addr1, addr2 };
}
// 测试套件
describe("Counter", function () {
// 测试用例
it("Should deploy with initial value 0", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
expect(await counter.x()).to.equal(0);
});
});
关键点说明:
-
导入语句:
- expect:从chai导入,用于断言
- network:从hardhat导入,用于网络连接
-
网络连接(Hardhat 3新方式):
- 使用await network.connect()获取ethers对象
- 这是与Hardhat 2的重要区别
- 同时获取networkHelpers用于快照恢复等功能
-
Fixture函数:
- 定义部署和初始化逻辑
- 返回测试需要的所有对象
- 使用networkHelpers.loadFixture()加载
3.3 测试组织结构
测试的组织结构是层次化的,使用describe和it来组织测试。
层次结构:
bash
describe("Counter", function () {
// 第一层:测试套件
describe("Deployment", function () {
// 第二层:子套件(按功能分组)
it("Should deploy with initial value 0", async function () {
// 第三层:具体测试用例
});
it("Should have valid address", async function () {
// 另一个测试用例
});
});
describe("Increment", function () {
// 另一个子套件
it("Should increment counter", async function () {
// 测试用例
});
});
});
组织原则:
- 按功能分组:使用describe按功能组织测试
- 描述性名称:使用清晰的描述性名称
- 保持独立:每个测试应该能够独立运行
- 避免依赖:测试之间不应该有依赖关系
3.4 Hardhat 3的特殊之处
在Hardhat 3中,有几个重要的变化:
- 网络连接方式:
bash
// Hardhat 2的方式(已废弃)
import { ethers } from "hardhat";
// Hardhat 3的方式(新)
const { ethers, networkHelpers } = await network.connect();
- 顶级await支持:
由于使用了ESM模块系统,可以在文件顶层使用await:
bash
// 可以在顶层使用await
const { ethers } = await network.connect();
- Fixture加载方式:
bash
// 使用networkHelpers.loadFixture
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
这些变化使得Hardhat 3的测试代码更加现代化和易用。
4. Mocha测试结构详解
4.1 钩子函数
Mocha提供了几个重要的钩子函数,用于在测试的不同阶段执行代码。
钩子函数类型:
-
before:
- 在所有测试前执行一次
- 适合初始化全局资源
- 例如:设置测试环境、初始化数据库连接
-
beforeEach:
- 在每个测试前执行
- 适合准备测试数据
- 例如:部署合约、获取签名者账户
-
afterEach:
- 在每个测试后执行
- 用于清理测试状态
- 例如:重置状态、清理临时数据
-
after:
- 在所有测试后执行一次
- 用于释放全局资源
- 例如:关闭数据库连接、清理临时文件
执行顺序:
bash
before (执行一次)
├── beforeEach
├── 测试用例1
├── afterEach
├── beforeEach
├── 测试用例2
├── afterEach
└── ...
after (执行一次)
4.2 钩子函数示例
beforeEach示例:
bash
describe("Counter", function () {
let counter: any;
let owner: any;
beforeEach(async function () {
// 每个测试前都会执行
[owner] = await ethers.getSigners();
counter = await ethers.deployContract("Counter");
});
it("Should increment", async function () {
await counter.inc();
expect(await counter.x()).to.equal(1);
});
it("Should set number", async function () {
await counter.setNumber(42);
expect(await counter.x()).to.equal(42);
});
});
注意:虽然可以使用beforeEach,但在Hardhat 3中,更推荐使用loadFixture,因为它提供了快照恢复功能,性能更好。
4.3 describe和it
describe:
describe用于定义测试套件,可以嵌套使用:
bash
describe("Counter", function () {
describe("Deployment", function () {
// 部署相关测试
});
describe("Increment", function () {
// 增量相关测试
});
});
it:
it用于定义具体的测试用例:
bash
it("Should increment counter", async function () {
// 测试逻辑
});
测试用例命名:
- 使用描述性的名称
- 描述测试场景和预期结果
- 例如:"Should revert when amount is zero"
4.4 异步测试
Mocha原生支持异步测试,可以使用Promise或async/await:
Promise方式:
bash
it("Should return a value", function () {
return someAsyncFunction().then(result => {
expect(result).to.equal(42);
});
});
async/await方式(推荐):
bash
it("Should return a value", async function () {
const result = await someAsyncFunction();
expect(result).to.equal(42);
});
在Hardhat 3中,我们主要使用async/await方式,因为它更清晰易读。
5. Chai断言语法基础
5.1 基础断言
Chai提供了丰富的断言方法,我们先来看看基础的断言。
相等性断言:
bash
expect(value).to.equal(42); // 等于
expect(value).to.not.equal(42); // 不等于
expect(value).to.be.deep.equal({a: 1}); // 深度相等(对象比较)
数值比较:
bash
expect(value).to.be.above(10); // 大于
expect(value).to.be.below(100); // 小于
expect(value).to.be.at.least(10); // 大于等于
expect(value).to.be.at.most(100); // 小于等于
布尔值断言:
bash
expect(value).to.be.true;
expect(value).to.be.false;
空值断言:
bash
expect(value).to.be.null;
expect(value).to.be.undefined;
expect(value).to.exist; // 不为null或undefined
类型检查:
bash
expect(value).to.be.a("string");
expect(value).to.be.an("array");
expect(value).to.be.an.instanceof(Contract);
5.2 链式断言
Chai的强大之处在于支持链式断言,我们可以将多个断言连接在一起:
bash
expect(value)
.to.be.a("bigint")
.and.to.be.above(0)
.and.to.be.below(1000000);
这种链式语法让代码更加清晰易读。
5.3 数组断言
数组类型和长度:
bash
expect(array).to.be.an("array");
expect(array).to.have.length(3);
包含元素:
bash
expect(array).to.include(2);
expect(array).to.contain(2);
5.4 对象断言
属性检查:
bash
expect(obj).to.have.property("name");
expect(obj).to.have.property("name", "value");
属性类型和值:
bash
expect(obj.name).to.be.a("string"); expect(obj.value).to.equal(42);
5.5 字符串断言
类型和内容:
bash
expect(str).to.be.a("string");
expect(str).to.include("substring");
expect(str).to.have.length(10);
expect(str).to.match(/^0x[a-fA-F0-9]{40}$/); // 正则匹配
地址验证示例:
bash
const address = await counter.getAddress();
expect(address).to.be.a("string");
expect(address).to.have.length(42);
expect(address).to.match(/^0x[a-fA-F0-9]{40}$/);
5.6 BigInt处理
在Solidity中,很多值是uint256类型,在JavaScript中对应bigint类型。需要注意类型匹配:
// 使用n后缀创建bigint
bash
expect(await counter.x()).to.equal(0n);
expect(await counter.x()).to.equal(42n);
// 或者使用BigInt构造函数
bash
expect(await counter.x()).to.equal(BigInt(0));
常见错误:
bash
// 错误:类型不匹配
expect(await counter.x()).to.equal(0); // 0是number类型
// 正确:使用bigint
expect(await counter.x()).to.equal(0n); // 0n是bigint类型
6. Hardhat 3特定断言
6.1 事件断言
事件是智能合约与外部世界通信的重要机制,因此测试事件触发也是非常重要的。
基础事件断言:
bash
await expect(counter.inc())
.to.emit(counter, "Increment");
验证事件参数:
bash
await expect(counter.inc())
.to.emit(counter, "Increment")
.withArgs(1n);
多个参数:
bash
await expect(counter.transfer(to, amount))
.to.emit(token, "Transfer")
.withArgs(from, to, amount);
注意事项:
- 必须使用await:事件断言是异步的,必须使用await
- 事件名称区分大小写:必须与合约中定义的一致
- 参数类型要匹配:特别是注意bigint类型
- 参数顺序要匹配:参数的顺序必须与事件定义一致
获取事件过滤器(推荐):
bash
// 使用合约实例的filters方法
const incrementFilter = counter.filters.Increment();
await expect(counter.inc())
.to.emit(counter, incrementFilter)
.withArgs(1n);
这种方式可以在编译时检查事件是否存在,更加安全。
6.2 余额变化断言
余额变化断言用于验证转账操作后,账户余额的变化是否符合预期。
单个账户余额变化:
bash
await expect(
owner.sendTransaction({ to: addr1.address, value: ethers.parseEther("1") })
).to.changeEtherBalance(addr1, ethers.parseEther("1"));
多个账户余额变化:
bash
await expect(
owner.sendTransaction({ to: addr1.address, value: ethers.parseEther("1") })
).to.changeEtherBalances(
[owner, addr1],
[ethers.parseEther("-1"), ethers.parseEther("1")]
);
考虑Gas费用:
// 自动考虑Gas费用
bash
await expect(
owner.sendTransaction({ to: addr1.address, value: ethers.parseEther("1") })
).to.changeEtherBalance(owner, ethers.parseEther("-1"), { includeFee: true });
注意事项:
- 余额变化包括Gas费用
- 使用includeFee选项可以自动考虑Gas费用
- 对于多个账户,数组长度要匹配
6.3 回退断言
在智能合约开发中,错误处理是非常重要的一部分。我们需要测试各种回退情况。
require回退(带错误消息):
bash
await expect(counter.incBy(0))
.to.be.revertedWith("incBy: increment should be positive");
无原因回退:
bash
await expect(contract.fail())
.to.be.reverted;
自定义错误:
bash
// 合约中定义自定义错误
// error InsufficientBalance(uint256 required, uint256 available);
await expect(contract.withdraw(amount))
.to.be.revertedWithCustomError(contract, "InsufficientBalance")
.withArgs(required, available);
Panic错误:
bash
// Panic代码:0x11 = 算术溢出
await expect(contract.overflow())
.to.be.revertedWithPanic(0x11);
常见Panic代码:
- 0x00:通用编译器插入的panic
- 0x01:断言失败
- 0x11:算术溢出
- 0x12:除零或模零
- 0x21:转换为不存在的枚举值
- 0x22:存储字节数组编码错误
- 0x31:在空数组上调用pop()
- 0x32:数组访问越界
- 0x41:分配太多内存
- 0x51:调用零初始化的变量
注意事项:
- 错误消息要精确匹配:revertedWith要求消息完全匹配,包括空格和大小写
- 测试所有回退路径:确保所有错误情况都被测试
- 验证安全机制:确保安全机制生效
- 使用描述性测试名称:让测试意图清晰
7. 合约部署和函数测试
7.1 部署测试
对于部署测试,我们需要验证合约是否正确部署,以及初始状态是否正确。
验证合约地址:
bash
it("Should have valid address", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
const address = await counter.getAddress();
expect(address).to.be.a("string");
expect(address).to.have.length(42);
expect(address).to.match(/^0x[a-fA-F0-9]{40}$/);
});
验证初始状态:
bash
it("Should deploy with initial value 0", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
expect(await counter.x()).to.equal(0n);
});
验证构造函数参数:
bash
it("Should deploy with correct constructor parameters", async function () {
const initialValue = 100n;
const counter = await ethers.deployContract("Counter", [initialValue]);
expect(await counter.x()).to.equal(initialValue);
});
7.2 函数测试
对于函数测试,我们应该测试正常流程、边界情况和异常情况。
正常流程测试:
bash
it("Should increment counter", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await counter.inc();
expect(await counter.x()).to.equal(1n);
await counter.inc();
expect(await counter.x()).to.equal(2n);
});
多次调用测试:
bash
it("Should increment by specific amount", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await counter.incBy(5);
expect(await counter.x()).to.equal(5n);
await counter.incBy(10);
expect(await counter.x()).to.equal(15n);
});
状态变化验证:
bash
it("Should update state correctly", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
const initialValue = await counter.x();
await counter.inc();
const newValue = await counter.x();
expect(newValue).to.equal(initialValue + 1n);
});
7.3 边界测试
边界情况往往是最容易出问题的地方,需要特别关注。
零值测试:
bash
it("Should handle zero value", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await counter.setNumber(0);
expect(await counter.x()).to.equal(0n);
});
最大值测试:
bash
it("Should handle maximum value", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
const maxValue = 2n ** 256n - 1n;
await counter.setNumber(maxValue);
expect(await counter.x()).to.equal(maxValue);
});
溢出测试:
bash
it("Should revert on overflow", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
const maxValue = 2n ** 256n - 1n;
await counter.setNumber(maxValue);
// 在Solidity 0.8+中,溢出会自动revert
await expect(counter.inc())
.to.be.revertedWithPanic(0x11); // 算术溢出
});
7.4 测试编写原则
在编写测试时,应该遵循以下原则:
- 每个函数至少一个测试:确保所有函数都被测试
- 测试正常和异常情况:覆盖所有代码路径
- 验证状态变化:确保状态正确更新
- 使用描述性名称:让测试意图清晰
- 保持测试独立:每个测试应该能够独立运行
8. 事件触发测试
8.1 基础事件测试
事件是智能合约与外部世界通信的重要机制,测试事件触发是验证合约行为的重要方式。
单个事件测试:
bash
it("Should emit Increment event", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await expect(counter.inc())
.to.emit(counter, "Increment")
.withArgs(1n);
});
事件参数验证:
bash
it("Should emit Increment event with correct parameters", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
const amount = 5n;
await expect(counter.incBy(amount))
.to.emit(counter, "Increment")
.withArgs(amount);
});
8.2 多个事件测试
如果一个交易触发了多个事件,我们需要分别验证:
bash
it("Should emit multiple events", async function () {
const { token, owner, addr1 } = await networkHelpers.loadFixture(deployTokenFixture);
const amount = ethers.parseEther("100");
const tx = await token.transfer(addr1.address, amount);
await expect(tx)
.to.emit(token, "Transfer")
.withArgs(owner.address, addr1.address, amount);
// 如果还有其他事件,继续验证
});
8.3 查询历史事件
我们还可以查询历史事件,这对于测试复杂的事件日志场景很有用:
bash
it("Should query historical events", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 记录部署时的区块号
const deployBlock = await ethers.provider.getBlockNumber();
// 执行一些操作
await counter.inc();
await counter.incBy(5);
// 查询事件
const filter = counter.filters.Increment();
const events = await counter.queryFilter(filter, deployBlock);
expect(events).to.have.length(2);
expect(events[0].args[0]).to.equal(1n);
expect(events[1].args[0]).to.equal(5n);
});
8.4 事件测试注意事项
注意事项:
- 事件必须在交易中触发:不能是纯view函数
- 使用await等待断言:事件断言是异步的
- 事件名称区分大小写:必须与合约中定义的一致
- 参数类型要匹配:特别是注意bigint类型
- 查询历史事件时需要记录区块号:用于指定查询范围
9. 错误和回退测试
9.1 require回退测试
require回退是最常见的错误处理方式,我们需要测试各种require条件。
带错误消息的require:
bash
it("Should revert when increment is zero", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await expect(counter.incBy(0))
.to.be.revertedWith("incBy: increment should be positive");
});
无参数require:
bash
it("Should revert without reason", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
await expect(contract.fail())
.to.be.reverted;
});
9.2 自定义错误测试
自定义错误是Solidity 0.8.4引入的特性,比字符串错误消息更省Gas。
合约中的自定义错误:
bash
error InsufficientBalance(uint256 required, uint256 available);
function withdraw(uint256 amount) public {
if (balance < amount) {
revert InsufficientBalance(amount, balance);
}
// ...
}
测试自定义错误:
bash
it("Should revert with custom error", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
const amount = ethers.parseEther("1000");
await expect(contract.withdraw(amount))
.to.be.revertedWithCustomError(contract, "InsufficientBalance")
.withArgs(amount, balance);
});
9.3 Panic错误测试
Panic错误是Solidity在某些严重错误时触发的,比如算术溢出、数组越界等。
算术溢出测试:
bash
it("Should revert with panic on overflow", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
const maxValue = 2n ** 256n - 1n;
await counter.setNumber(maxValue);
// 在Solidity 0.8+中,溢出会触发panic
await expect(counter.inc())
.to.be.revertedWithPanic(0x11); // 算术溢出
});
数组越界测试:
bash
it("Should revert with panic on array out of bounds", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
await expect(contract.getArrayElement(100))
.to.be.revertedWithPanic(0x32); // 数组访问越界
});
9.4 错误测试最佳实践
测试原则:
- 错误消息要精确匹配:revertedWith要求消息完全匹配
- 测试所有回退路径:确保所有错误情况都被测试
- 验证安全机制:确保安全机制生效
- 使用描述性测试名称:让测试意图清晰
测试覆盖:
- 正常流程
- 边界情况
- 异常情况
- 所有require条件
- 所有自定义错误
- 所有可能的panic情况
10. 时间和区块操作
10.1 时间操作
有些智能合约依赖于时间或区块号,比如时间锁合约、定时任务等。Hardhat提供了强大的时间旅行和区块操作功能。
增加时间:
bash
import { time } from "@nomicfoundation/hardhat-network-helpers";
it("Should allow withdrawal after lock period", async function () {
const { vault } = await networkHelpers.loadFixture(deployVaultFixture);
const lockPeriod = 7 * 24 * 60 * 60; // 7天(秒)
// 增加7天
await time.increase(lockPeriod);
// 现在可以提取了
await expect(vault.withdraw())
.to.not.be.reverted;
});
跳转到特定时间:
bash
it("Should allow action at specific time", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
const targetTime = Math.floor(Date.now() / 1000) + 86400; // 24小时后
await time.increaseTo(targetTime);
await expect(contract.execute())
.to.not.be.reverted;
});
设置下一个区块的时间戳:
bash
it("Should set next block timestamp", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
const targetTime = Math.floor(Date.now() / 1000) + 3600;
await time.setNextBlockTimestamp(targetTime);
await time.advanceBlock();
// 现在区块时间戳是targetTime
});
10.2 区块操作
挖掘区块:
bash
import { mine } from "@nomicfoundation/hardhat-network-helpers";
it("Should mine blocks", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
const initialBlock = await ethers.provider.getBlockNumber();
// 挖掘5个区块
await mine(5);
const finalBlock = await ethers.provider.getBlockNumber();
expect(finalBlock).to.equal(initialBlock + 5);
});
挖掘到特定区块号:
bash
import { mineUpTo } from "@nomicfoundation/hardhat-network-helpers";
it("Should mine up to specific block", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
const targetBlock = 100;
await mineUpTo(targetBlock);
const currentBlock = await ethers.provider.getBlockNumber();
expect(currentBlock).to.be.at.least(targetBlock);
});
10.3 时间锁合约测试示例
时间锁合约:
bash
contract Timelock {
uint256 public constant LOCK_PERIOD = 7 days;
uint256 public lockedUntil;
mapping(address => uint256) public balances;
function lock() external {
lockedUntil = block.timestamp + LOCK_PERIOD;
}
function withdraw() external {
require(block.timestamp >= lockedUntil, "Still locked");
// 提取逻辑
}
}
测试时间锁:
bash
it("Should prevent withdrawal before lock period", async function () {
const { timelock } = await networkHelpers.loadFixture(deployTimelockFixture);
await timelock.lock();
// 尝试立即提取(应该失败)
await expect(timelock.withdraw())
.to.be.revertedWith("Still locked");
});
it("Should allow withdrawal after lock period", async function () {
const { timelock } = await networkHelpers.loadFixture(deployTimelockFixture);
await timelock.lock();
// 增加7天
await time.increase(7 * 24 * 60 * 60);
// 现在可以提取了
await expect(timelock.withdraw())
.to.not.be.reverted;
});
10.4 注意事项
注意事项:
- 这些操作只在Hardhat网络有效:不会影响真实的区块链
- 时间操作是异步的:需要使用await
- 区块操作会影响区块号:可能影响依赖区块号的逻辑
- 时间戳必须是递增的:不能设置比当前时间更早的时间戳
11. 快照和恢复
11.1 loadFixture概述
在编写测试时,我们经常需要在每个测试前部署合约。如果使用beforeEach,每次测试都会重新部署,这会消耗大量时间。Hardhat提供了loadFixture功能来解决这个问题。
loadFixture的工作原理:
- 第一次调用时,执行Fixture函数并创建快照
- 后续调用时,直接恢复到快照状态,而不是重新执行
- 这可以将测试速度提升5到10倍
11.2 Fixture函数定义
Fixture函数应该是一个纯函数,返回所有测试需要的对象:
bash
async function deployCounterFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const counter = await ethers.deployContract("Counter");
// 可选:执行一些初始化操作
await counter.incBy(10);
return { counter, owner, addr1, addr2 };
}
Fixture函数原则:
- 应该是纯函数:相同的输入应该产生相同的输出
- 只进行初始化:不应该在Fixture中执行测试逻辑
- 返回所有需要的对象:包括合约、账户等
- 可以处理复杂逻辑:包括多合约部署和预设状态
11.3 使用loadFixture
基础使用:
bash
it("Should use fixture", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 使用counter进行测试
expect(await counter.x()).to.equal(10n);
});
多个测试使用同一个Fixture:
bash
describe("Counter", function () {
it("Test 1", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 测试1
});
it("Test 2", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 测试2 - 状态是干净的,不受测试1影响
});
});
11.4 复杂Fixture示例
多合约系统Fixture:
bash
async function deployVaultSystemFixture() {
const [owner, user1, user2] = await ethers.getSigners();
// 部署Token合约
const token = await ethers.deployContract("Token");
// 部署Vault合约,传入Token地址
const vault = await ethers.deployContract("Vault", [await token.getAddress()]);
// 给用户分配一些Token
await token.transfer(user1.address, ethers.parseEther("1000"));
await token.transfer(user2.address, ethers.parseEther("2000"));
return { token, vault, owner, user1, user2 };
}
11.5 loadFixture的优势
优势总结:
- 每个测试独立运行:不受其他测试影响
- 保证测试可重复性:每次运行都是相同的初始状态
- 避免重复部署:大幅提升测试速度
- 可以处理复杂初始化:包括多合约部署和预设状态
性能对比:
- 使用beforeEach:每个测试都重新部署,100个测试可能需要几分钟
- 使用loadFixture:第一次部署,后续恢复快照,100个测试可能只需要几十秒
12. 测试覆盖率
12.1 覆盖率概述
测试覆盖率是衡量测试质量的重要指标。它告诉我们测试覆盖了多少代码,帮助我们识别未测试的代码路径。
覆盖率类型:
- 语句覆盖率:执行了多少条语句
- 分支覆盖率:执行了多少个分支
- 函数覆盖率:调用了多少个函数
- 行覆盖率:执行了多少行代码
12.2 配置覆盖率
在Hardhat 3中,覆盖率功能已经集成在toolbox中,不需要额外配置。
运行覆盖率测试:
bash
# 生成覆盖率报告
npx hardhat test --coverage
# 生成HTML报告
npx hardhat test --coverage --report html
# 只运行Mocha测试并生成覆盖率
npx hardhat test mocha --coverage
# 只运行Solidity测试并生成覆盖率
npx hardhat test solidity --coverage
12.3 覆盖率报告
控制台报告:
运行覆盖率测试后,会在控制台显示覆盖率统计:
bash
Coverage Report
===============
Statements : 95.45% ( 21/22 )
Branches : 90.00% ( 18/20 )
Functions : 100.00% ( 10/10 )
Lines : 95.45% ( 21/22 )
HTML报告:
使用--report html选项可以生成HTML报告:
bash
npx hardhat test --coverage --report html
这会在coverage目录下生成HTML报告,可以在浏览器中打开查看详细的覆盖率信息。
12.4 覆盖率目标
推荐覆盖率:
- 关键合约:应该达到100%的覆盖率
- 一般合约:应该超过80%
- 工具合约:可以稍微低一些,但最好也超过70%
覆盖率不是唯一指标:
- 高覆盖率不代表测试质量高
- 需要测试有意义的场景
- 关注边界情况和异常情况
12.5 提高覆盖率
识别未覆盖的代码:
- 查看覆盖率报告
- 识别未覆盖的代码路径
- 编写测试覆盖这些路径
常见未覆盖场景:
- 错误处理路径
- 边界条件
- 特殊状态转换
- 权限检查
13. Gas报告
13.1 Gas报告概述
除了功能测试,我们还需要关注Gas消耗。Gas消耗直接影响合约部署和运行的成本,特别是在主网上。
Gas报告的作用:
- 识别高Gas函数
- 对比不同实现方式
- 优化Gas消耗
- 估算部署和运行成本
13.2 生成Gas报告
使用--gas-stats选项:
bash
npx hardhat test --gas-stats
这会显示每个函数的Gas消耗统计:
- 最小值
- 平均值
- 中位数
- 最大值
- 调用次数
- 部署成本和大小
使用环境变量:
bash
REPORT_GAS=true npx hardhat test
13.3 Gas报告示例
报告输出:
bash
Gas Report
==========
Counter.inc()
Min: 26,234
Avg: 26,234
Median: 26,234
Max: 26,234
# calls: 5
Counter.incBy(uint256)
Min: 26,456
Avg: 26,456
Median: 26,456
Max: 26,456
# calls: 3
13.4 Gas优化建议
优化技巧:
- 减少存储操作:存储操作是最耗Gas的
- 使用事件代替存储:对于不需要链上查询的数据
- 优化数据结构:使用packed storage
- 减少外部调用:批量处理操作
- 使用库函数:复用代码,减少部署大小
对比测试:
bash
it("Should compare gas costs", async function () {
const { contract } = await networkHelpers.loadFixture(deployFixture);
// 测试方法1
const tx1 = await contract.method1();
const receipt1 = await tx1.wait();
const gas1 = receipt1.gasUsed;
// 测试方法2
const tx2 = await contract.method2();
const receipt2 = await tx2.wait();
const gas2 = receipt2.gasUsed;
console.log(`Method 1: ${gas1}, Method 2: ${gas2}`);
});
14. 测试最佳实践
14.1 命名和组织
描述性命名:
测试名称应该清晰描述测试场景和预期结果:
bash
// 好的命名
it("Should revert when amount is zero", async function () {});
it("Should emit Transfer event when transferring tokens", async function () {});
// 不好的命名
it("Test 1", async function () {});
it("Should work", async function () {});
合理组织:
按功能分组测试,使用嵌套describe:
bash
describe("Counter", function () {
describe("Deployment", function () {
// 部署相关测试
});
describe("Increment", function () {
// 增量相关测试
});
describe("Error Handling", function () {
// 错误处理相关测试
});
});
14.2 测试独立性
保持独立:
每个测试应该能够独立运行,不依赖其他测试:
bash
// 好的:每个测试独立
it("Test 1", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 测试1
});
it("Test 2", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 测试2 - 不依赖测试1
});
// 不好的:测试之间有依赖
let counter;
it("Test 1", async function () {
counter = await ethers.deployContract("Counter");
});
it("Test 2", async function () {
// 依赖测试1的counter
await counter.inc();
});
使用loadFixture:
使用loadFixture可以确保每个测试都有干净的初始状态:
bash
it("Should not be affected by previous test", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 即使前面的测试修改了状态,这里也是干净的
expect(await counter.x()).to.equal(0n);
});
14.3 全面覆盖
测试所有路径:
- 正常流程
- 异常情况
- 边界条件
- 所有函数
- 所有错误处理
边界测试:
bash
it("Should handle zero value", async function () {});
it("Should handle maximum value", async function () {});
it("Should revert on overflow", async function () {});
14.4 错误处理
验证错误消息:
bash
await expect(contract.fail())
.to.be.revertedWith("Expected error message");
测试所有回退路径:
bash
it("Should revert when condition 1 fails", async function () {});
it("Should revert when condition 2 fails", async function () {});
it("Should revert when condition 3 fails", async function () {});
使用自定义错误:
自定义错误比字符串错误消息更省Gas:
bash
await expect(contract.fail())
.to.be.revertedWithCustomError(contract, "CustomError");
14.5 性能优化
使用快照恢复:
使用loadFixture而不是beforeEach:
bash
// 好的:使用loadFixture
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 不好的:每次都重新部署
beforeEach(async function () {
counter = await ethers.deployContract("Counter");
});
并行运行测试:
bash
npx hardhat test --parallel
14.6 代码复用
提取公共逻辑:
bash
// 提取到Fixture
async function deployCounterFixture() {
// 公共部署逻辑
}
// 提取辅助函数
async function setupCounter(initialValue: bigint) {
const counter = await ethers.deployContract("Counter");
await counter.setNumber(initialValue);
return counter;
}
14.7 文档化
添加注释:
bash
it("Should handle complex scenario", async function () {
// 这个测试验证了在特定条件下,合约的行为
// 条件:余额 > 1000,时间 > lockPeriod
const { contract } = await networkHelpers.loadFixture(deployFixture);
// 设置条件
await contract.deposit(ethers.parseEther("2000"));
await time.increase(lockPeriod);
// 验证行为
await expect(contract.withdraw())
.to.not.be.reverted;
});
使用描述性变量名:
bash
// 好的
const initialBalance = await token.balanceOf(owner.address);
const transferAmount = ethers.parseEther("100");
// 不好的
const b1 = await token.balanceOf(owner.address);
const amt = ethers.parseEther("100");
15. 常见错误和解决方案
15.1 忘记使用await
问题:忘记使用await等待异步操作
bash
// 错误
expect(counter.inc()).to.emit(counter, "Increment");
// 正确
await expect(counter.inc()).to.emit(counter, "Increment");
解决方案:确保所有异步操作都使用await
15.2 事件名称错误
问题:事件名称大小写不匹配
bash
// 错误:事件名称大小写不匹配
await expect(counter.inc())
.to.emit(counter, "increment"); // 应该是"Increment"
// 正确
await expect(counter.inc())
.to.emit(counter, "Increment");
解决方案:使用合约实例的filters方法
bash
const incrementFilter = counter.filters.Increment();
await expect(counter.inc())
.to.emit(counter, incrementFilter);
15.3 参数类型不匹配
问题:JavaScript的number类型与Solidity的uint256不匹配
bash
// 错误:类型不匹配
expect(await counter.x()).to.equal(0); // 0是number类型
// 正确:使用bigint
expect(await counter.x()).to.equal(0n); // 0n是bigint类型
解决方案:
bash
使用n后缀创建bigint:0n, 42n
使用BigInt()构造函数:BigInt(0)
使用ethers工具函数:ethers.parseEther("1")
15.4 错误消息不匹配
bash
问题:revertWith要求消息完全匹配
// 错误:消息不完全匹配
await expect(contract.fail())
.to.be.revertedWith("Error"); // 实际消息是"Error: invalid input"
// 正确:完全匹配
await expect(contract.fail())
.to.be.revertedWith("Error: invalid input");
解决方案:
确保错误消息完全匹配,包括空格和大小写
如果消息可能变化,考虑只使用.to.be.reverted
15.5 网络连接问题
问题:无法连接到网络
解决方案:
检查hardhat.config.ts中的网络配置
确保使用正确的网络类型(edr-simulated或http)
检查环境变量是否正确设置
确认RPC URL有效
15.6 合约未部署
问题:在测试中使用合约前没有部署
bash
// 错误:合约未部署
it("Should work", async function () {
await counter.inc(); // counter未定义
});
// 正确:先部署合约
it("Should work", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await counter.inc();
});
解决方案:
确保在测试前部署合约,或使用Fixture管理部署
16. 实战演示
16.1 完整测试示例
让我们看一个完整的测试示例,展示所有学到的技巧:
Counter合约:
bash
// contracts/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 public x;
event Increment(uint256 by);
constructor() {
x = 0;
}
function inc() public {
x++;
emit Increment(1);
}
function incBy(uint256 by) public {
require(by > 0, "incBy: increment should be positive");
x += by;
emit Increment(by);
}
function setNumber(uint256 _x) public {
x = _x;
}
}
完整测试文件:
bash
// test/Counter.test.ts
import { expect } from "chai";
import { network } from "hardhat";
// 连接网络
const { ethers, networkHelpers } = await network.connect();
// 定义Fixture函数
async function deployCounterFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const counter = await ethers.deployContract("Counter");
return { counter, owner, addr1, addr2 };
}
// 测试套件
describe("Counter", function () {
// 子套件:部署测试
describe("Deployment", function () {
// 测试用例:初始值测试
it("Should deploy with initial value 0", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
expect(await counter.x()).to.equal(0n);
});
// 测试用例:合约地址验证
it("Should have valid address", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
const address = await counter.getAddress();
expect(address).to.be.a("string");
expect(address).to.have.length(42);
expect(address).to.match(/^0x[a-fA-F0-9]{40}$/);
});
});
// 子套件:增量功能测试
describe("Increment", function () {
// 测试用例:基本增量
it("Should increment counter", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await counter.inc();
expect(await counter.x()).to.equal(1n);
await counter.inc();
expect(await counter.x()).to.equal(2n);
});
// 测试用例:指定增量
it("Should increment by specific amount", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await counter.incBy(5n);
expect(await counter.x()).to.equal(5n);
await counter.incBy(10n);
expect(await counter.x()).to.equal(15n);
});
// 测试用例:事件触发
it("Should emit Increment event", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await expect(counter.inc())
.to.emit(counter, "Increment")
.withArgs(1n);
await expect(counter.incBy(5n))
.to.emit(counter, "Increment")
.withArgs(5n);
});
// 测试用例:错误处理
it("Should revert when increment is zero", async function () {
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
await expect(counter.incBy(0))
.to.be.revertedWith("incBy: increment should be positive");
});
});
// 子套件:状态隔离测试
describe("State Isolation", function () {
// 测试用例:测试隔离验证
it("Should not be affected by previous test", async function () {
// 即使前面的测试修改了状态,这里也是干净的
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
// 从0开始,不是从之前测试的值开始
expect(await counter.x()).to.equal(0n);
});
});
});
16.2 运行测试
运行所有测试:
bash
npx hardhat test
运行特定测试文件:
bash
npx hardhat test test/Counter.test.ts
运行匹配的测试:
bash
npx hardhat test --grep "Deployment"
生成Gas报告:
bash
npx hardhat test --gas-stats
生成覆盖率报告:
bash
npx hardhat test --coverage
17.官方参考文档
Hardhat测试文档:https://hardhat.org/docs/testing
Chai断言库文档:https://www.chaijs.com/
Mocha测试框架文档:https://mochajs.org/