HelloWorld 合约测试教程

要测试 HelloWorld 合约,最直接的做法是:在本地用 Hardhat 建一个项目,写一个简单的 HelloWorld.sol(包含状态变量、setter/getter 与事件),用 ethers.js + mocha/chai 编写单元测试覆盖正常路径、异常路径与边界值,运行 Hardhat Network(或 Ganache)并查看断言与覆盖率报告;最后补上模拟时间、快照和回滚测试,确保在升级、重入、防御性编程等方面没有盲点。

HelloWorld 合约测试教程

先把“为什么要测试”讲清楚

合约一旦部署到链上,代码不可更改(除非用了代理模式),资金风险真实存在。*测试不是为了证明代码完美,而是把已知风险降到最低*。想象一下:一个简单的 HelloWorld 合约看似平凡,但状态读写、事件触发、函数可见性、权限检查、重入与异常处理等每一项都可能出问题。通过系统化测试,你能在本地复现多种场景,捕获逻辑错误、断言不成立、边界条件和异常路径。

准备工作和工具

  • Node.js 与包管理器:Node 14+,npm 或 yarn。
  • 开发框架:Hardhat(推荐)或 Truffle。
  • 客户端/本地链:Hardhat Network、Ganache CLI/GUI。
  • 测试库:mocha(测试框架)、chai(断言)、ethers.js(与合约交互)或 web3.js。
  • 覆盖率与静态分析:solidity-coverage、solhint、slither(可选,slither 需要 Python 环境)。
  • 持续集成:GitHub Actions / GitLab CI(把测试作为 pipeline 步骤)。

从零开始:搭建一个 Hardhat 项目

步骤很简单,我就按常见流程把命令列出来,这样一边做一边能快速上手:

mkdir hello-test
cd hello-test
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat

运行 npx hardhat 后选择“Create a basic sample project”,这样会生成示例合约、测试和配置文件,方便参考。

示例合约:HelloWorld.sol

合约非常简单,但为了演示测试点,我会加上事件、权限和一个会改变状态的函数:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract HelloWorld { string private message; address public owner;

event MessageChanged(address indexed changer, string oldMessage, string newMessage);

constructor(string memory _message) {
    message = _message;
    owner = msg.sender;
}

function getMessage() public view returns (string memory) {
    return message;
}

function setMessage(string memory _new) public {
    string memory old = message;
    message = _new;
    emit MessageChanged(msg.sender, old, _new);
}

function restrictedSet(string memory _new) public {
    require(msg.sender == owner, "Only owner");
    message = _new;
}

}

为什么这样写?

我把三个点放进合约:读函数(getter)、普通写函数(带事件)和受限写函数(权限检查)。这些覆盖了常见的测试维度:返回值、事件校验、权限拒绝路径。

编写单元测试(ethers + mocha + chai)

测试文件放在 test/ 目录,命名为 hello.test.js(或 .ts)。关键点是:部署合约、调用函数、断言值、监听事件和断言 revert 情况。

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("HelloWorld", function () { let Hello, hw, owner, addr1;

beforeEach(async function () { Hello = await ethers.getContractFactory("HelloWorld"); [owner, addr1] = await ethers.getSigners(); hw = await Hello.deploy("Hi"); await hw.deployed(); });

it("初始消息应正确", async function () { expect(await hw.getMessage()).to.equal("Hi"); });

it("setMessage 应触发事件并更新消息", async function () { await expect(hw.connect(addr1).setMessage("Hello")) .to.emit(hw, "MessageChanged") .withArgs(addr1.address, "Hi", "Hello"); expect(await hw.getMessage()).to.equal("Hello"); });

it("restrictedSet 只能被 owner 调用", async function () { await expect(hw.connect(addr1).restrictedSet("X")).to.be.revertedWith("Only owner"); await hw.restrictedSet("OwnerHello"); expect(await hw.getMessage()).to.equal("OwnerHello"); }); });

测试要点说明

  • 部署后的状态:检查 constructor 设置的值(owner、初始消息)。
  • 事件断言:验证事件是否被触发、indexed 参数是否正确。
  • 拒绝路径:用 .revertedWith 检查 require 的错误信息。
  • 保持测试小且可复用:beforeEach 部署新合约,保证测试隔离。

常用命令速查表

命令 说明
npx hardhat test 运行所有测试(默认使用 Hardhat Network)
npx hardhat node 开启本地节点,方便用外部脚本或前端连接
npx hardhat coverage 生成测试覆盖率(需要 solidity-coverage)

进阶测试场景

简单的读写测试覆盖了基本逻辑,但生产合约通常需要更多场景的验证:

  • 时间相关逻辑:用 Hardhat 的 evm_increaseTime 和 evm_mine 模拟时间流逝来测试锁定期、拍卖等功能。
  • 快照与回滚:在复杂场景里先 snapshot,再做多个操作,最后回滚以便重用链状态,提高测试速度。
  • 重入与安全边界:为易受攻击的函数编写对手合约,模拟攻击路径。
  • 主网 forking:把主网状态 fork 到本地,测试合约与现有链上合约的交互(例如代币合约)。
  • 模糊测试与属性测试:用不同输入快速验证不变量,例如“消息长度若超过 X 应 revert”。

覆盖率、静态分析与性能

测试完成后,静态分析和覆盖率能提供额外信心:solidity-coverage 报告告诉你哪些分支未被覆盖;slither 可以找出常见安全问题(如未使用的变量、可重入风险)。另一个可选项是测量 gas 消耗,避免函数在大量调用下成本过高。

常见误区与调试技巧

  • 误区:只测试“成功路径”。现实问题往往在异常路径,所以要主动写失败测试。
  • 误区:忽视事件参数的 indexed 差异。indexed 参数在断言时需要注意 order 和格式。
  • 调试技巧:使用 console.log(Hardhat 提供 console.log 支持)在合约中打印变量,或在测试中打印 tx.receipt 来查看 gasUsed。
  • 调试技巧二:针对复现困难的 bug,建立最小可复现合约和测试,逐步剥离无关代码。

测试组织与最佳实践

  • 每个合约一个测试文件,按功能拆分测试用例(正常、异常、边界)。
  • 用 fixtures(或 beforeEach)部署独立实例,保证测试互不干扰。
  • 把常用断言封装成 helper 函数,减少重复代码。
  • 在 CI 中执行测试、覆盖率和静态分析,任何失败都阻止合并。

最后说几句实操感想

写完这些测试后,你会发现最宝贵的并不是绿灯的数量,而是在写测试的过程中暴露出的设计问题。经常有些函数在测试时显得“难用”或“容易出错”,那是告诉你应该在合约层面优化接口或增加更明确的错误信息。顺便一提,别把测试当成最后一步——把它视作设计的一部分,会让代码更健壮,也更容易维护。

返回首页