区块链智能合约测试:Truffle与Hardhat实战_千锋教育

来源:千锋教育
发布时间:2026-07-03 18:40:01

千锋教育品牌logo

01 为什么智能合约测试不是“锦上添花”,而是“生死存亡”

2022年,跨链桥Ronin被黑,损失6.2亿美元。2023年,Curve Finance重入攻击,2.5亿美元蒸发。这些事故的共性?智能合约代码中的逻辑漏洞,在部署前未被充分测试。

区块链的特性决定了智能合约一旦部署,代码不可篡改。传统软件可以发补丁,智能合约只能硬分叉或直接放弃。这意味着,测试不是开发流程的附属品,而是资产安全的最后一道防线。

智能合约测试与传统Web测试有本质区别:

  • ►确定性:同一输入必定产生同一输出,测试结果可精确复现
  • ►状态敏感:每个交易改变合约状态,测试需模拟完整生命周期
  • ►Gas成本:代码效率直接影响执行成本,测试需验证Gas消耗
  • ►不可逆性:无法回滚已确认的交易,测试必须覆盖边界情况

代码即法律,测试即法庭。没有经过审判的法律,就是暴政。

数据不会说谎:根据DeFi安全报告,2023年所有重大安全事件中,78%源于未经充分测试的逻辑漏洞。其中,重入攻击占32%,访问控制漏洞占28%,算术溢出占15%。

这意味着什么?如果你跳过测试,你有78%的概率在部署后3个月内遭遇攻击。这不是危言耸听,这是统计概率。

02 Truffle:老牌框架的完整测试体系

Truffle是智能合约开发领域的老牌框架,从2015年诞生至今,积累了庞大的生态和社区支持。它的测试体系基于Mocha和Chai,对JavaScript开发者极其友好。

环境搭建

bash
npm install -g truffle
truffle init my-project
cd my-project

编写测试合约

创建一个简单的代币合约 contracts/MyToken.sol:

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

contract MyToken {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor(uint256 _initialSupply) {
        totalSupply = _initialSupply * 10 ** decimals;
        balanceOf[msg.sender] = totalSupply;
    }

    function transfer(address _to, uint256 _value) public returns (bool success) {
        require(balanceOf[msg.sender] >= _value, "Insufficient balance");
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
        emit Transfer(msg.sender, _to, _value);
        return true;
    }

    function approve(address _spender, uint256 _value) public returns (bool success) {
        allowance[msg.sender][_spender] = _value;
        emit Approval(msg.sender, _spender, _value);
        return true;
    }

    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        require(balanceOf[_from] >= _value, "Insufficient balance");
        require(allowance[_from][msg.sender] >= _value, "Allowance exceeded");
        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        allowance[_from][msg.sender] -= _value;
        emit Transfer(_from, _to, _value);
        return true;
    }
}

编写Truffle测试

在 test/ 目录下创建 TestMyToken.js:

javascript
const MyToken = artifacts.require("MyToken");

contract("MyToken", (accounts) => {
    const [owner, user1, user2] = accounts;
    const initialSupply = 1000;

    beforeEach(async () => {
        this.token = await MyToken.new(initialSupply);
    });

    describe("代币基本信息", () => {
        it("应该正确设置名称和符号", async () => {
            const name = await this.token.name();
            const symbol = await this.token.symbol();
            assert.equal(name, "MyToken");
            assert.equal(symbol, "MTK");
        });

        it("应该正确分配初始供应量", async () => {
            const balance = await this.token.balanceOf(owner);
            const totalSupply = await this.token.totalSupply();
            assert.equal(balance.toString(), totalSupply.toString());
            assert.equal(totalSupply.toString(), (initialSupply * 10 ** 18).toString());
        });
    });

    describe("转账功能", () => {
        it("应该成功转账", async () => {
            const amount = web3.utils.toWei("100", "ether");
            await this.token.transfer(user1, amount, { from: owner });
            const user1Balance = await this.token.balanceOf(user1);
            assert.equal(user1Balance.toString(), amount);
        });

        it("余额不足时应拒绝转账", async () => {
            const amount = web3.utils.toWei("2000", "ether");
            try {
                await this.token.transfer(user1, amount, { from: owner });
                assert.fail("应该抛出异常");
            } catch (error) {
                assert(error.message.includes("Insufficient balance"));
            }
        });

        it("转账应该触发事件", async () => {
            const amount = web3.utils.toWei("50", "ether");
            const result = await this.token.transfer(user1, amount, { from: owner });
            assert.equal(result.logs[0].event, "Transfer");
            assert.equal(result.logs[0].args.from, owner);
            assert.equal(result.logs[0].args.to, user1);
        });
    });

    describe("授权与代理转账", () => {
        it("应该设置授权额度", async () => {
            const amount = web3.utils.toWei("100", "ether");
            await this.token.approve(user1, amount, { from: owner });
            const allowance = await this.token.allowance(owner, user1);
            assert.equal(allowance.toString(), amount);
        });

        it("应该使用授权额度完成代理转账", async () => {
            const approveAmount = web3.utils.toWei("100", "ether");
            const transferAmount = web3.utils.toWei("50", "ether");
            
            await this.token.approve(user1, approveAmount, { from: owner });
            await this.token.transferFrom(owner, user2, transferAmount, { from: user1 });
            
            const user2Balance = await this.token.balanceOf(user2);
            assert.equal(user2Balance.toString(), transferAmount);
        });

        it("授权额度不足时应拒绝代理转账", async () => {
            const approveAmount = web3.utils.toWei("10", "ether");
            const transferAmount = web3.utils.toWei("50", "ether");
            
            await this.token.approve(user1, approveAmount, { from: owner });
            try {
                await this.token.transferFrom(owner, user2, transferAmount, { from: user1 });
                assert.fail("应该抛出异常");
            } catch (error) {
                assert(error.message.includes("Allowance exceeded"));
            }
        });
    });
});

运行测试

bash
truffle test

输出示例:

text
Contract: MyToken
  代币基本信息
    ✓ 应该正确设置名称和符号 (45ms)
    ✓ 应该正确分配初始供应量 (52ms)
  转账功能
    ✓ 应该成功转账 (48ms)
    ✓ 余额不足时应拒绝转账 (41ms)
    ✓ 转账应该触发事件 (53ms)
  授权与代理转账
    ✓ 应该设置授权额度 (44ms)
    ✓ 应该使用授权额度完成代理转账 (56ms)
    ✓ 授权额度不足时应拒绝代理转账 (38ms)

  8 passing (2s)

Truffle的优势在于简洁的API和成熟的生态系统。但它的缺点也明显:测试速度慢,每次测试都部署新合约;调试困难,错误信息不够直观。

03 Hardhat:新一代测试框架的速度与灵活性

Hardhat是2019年推出的新一代开发框架,迅速成为行业标准。它的测试速度比Truffle快3-5倍,调试能力更强。

环境搭建

bash
npm install --save-dev hardhat
npx hardhat init

选择"Create an empty hardhat.config.js"

配置Hardhat

编辑 hardhat.config.js:

javascript
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.19",
  networks: {
    hardhat: {
      chainId: 1337,
    },
  },
  gasReporter: {
    enabled: true,
    currency: "USD",
  },
};

编写Hardhat测试

在 test/ 目录下创建 MyTokenTest.js:

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

describe("MyToken", function () {
    let token;
    let owner, user1, user2;

    beforeEach(async function () {
        [owner, user1, user2] = await ethers.getSigners();
        const MyToken = await ethers.getContractFactory("MyToken");
        token = await MyToken.deploy(1000);
        await token.deployed();
    });

    describe("代币基本信息", function () {
        it("应该正确设置名称和符号", async function () {
            expect(await token.name()).to.equal("MyToken");
            expect(await token.symbol()).to.equal("MTK");
        });

        it("应该正确分配初始供应量", async function () {
            const ownerBalance = await token.balanceOf(owner.address);
            const totalSupply = await token.totalSupply();
            expect(ownerBalance).to.equal(totalSupply);
            expect(totalSupply).to.equal(ethers.utils.parseEther("1000"));
        });
    });

    describe("转账功能", function () {
        it("应该成功转账", async function () {
            const amount = ethers.utils.parseEther("100");
            await token.connect(owner).transfer(user1.address, amount);
            expect(await token.balanceOf(user1.address)).to.equal(amount);
        });

        it("余额不足时应拒绝转账", async function () {
            const amount = ethers.utils.parseEther("2000");
            await expect(
                token.connect(owner).transfer(user1.address, amount)
            ).to.be.revertedWith("Insufficient balance");
        });

        it("转账应该触发事件", async function () {
            const amount = ethers.utils.parseEther("50");
            await expect(token.connect(owner).transfer(user1.address, amount))
                .to.emit(token, "Transfer")
                .withArgs(owner.address, user1.address, amount);
        });
    });

    describe("授权与代理转账", function () {
        it("应该设置授权额度", async function () {
            const amount = ethers.utils.parseEther("100");
            await token.connect(owner).approve(user1.address, amount);
            expect(await token.allowance(owner.address, user1.address)).to.equal(amount);
        });

        it("应该使用授权额度完成代理转账", async function () {
            const approveAmount = ethers.utils.parseEther("100");
            const transferAmount = ethers.utils.parseEther("50");
            
            await token.connect(owner).approve(user1.address, approveAmount);
            await token.connect(user1).transferFrom(owner.address, user2.address, transferAmount);
            
            expect(await token.balanceOf(user2.address)).to.equal(transferAmount);
        });

        it("授权额度不足时应拒绝代理转账", async function () {
            const approveAmount = ethers.utils.parseEther("10");
            const transferAmount = ethers.utils.parseEther("50");
            
            await token.connect(owner).approve(user1.address, approveAmount);
            await expect(
                token.connect(user1).transferFrom(owner.address, user2.address, transferAmount)
            ).to.be.revertedWith("Allowance exceeded");
        });
    });
});

运行测试

bash
npx hardhat test

输出示例:

text
  MyToken
    代币基本信息
      ✓ 应该正确设置名称和符号
      ✓ 应该正确分配初始供应量
    转账功能
      ✓ 应该成功转账
      ✓ 余额不足时应拒绝转账
      ✓ 转账应该触发事件
    授权与代理转账
      ✓ 应该设置授权额度
      ✓ 应该使用授权额度完成代理转账
      ✓ 授权额度不足时应拒绝代理转账

  8 passing (1s)

·-----------------|----------------------------·-----------------------------|
|  Solc version: 0.8.19 ·  Optimizer enabled: false  ·  Runs: 200  ·  Block limit: 30000000 gas  |
·················|·············································|··············
|  Methods                                                                     |
·················|··············|······························|··············
|  Contract      ·  Method      ·  Min            ·  Max       ·  Avg         |
·················|··············|······························|··············
|  MyToken       ·  approve     ·  46284          ·  46284     ·  46284       |
·················|··············|······························|··············
|  MyToken       ·  transfer    ·  51790          ·  51790     ·  51790       |
·················|··············|······························|··············
|  MyToken       ·  transferFrom·  60426          ·  60426     ·  60426       |
·················|··············|······························|··············
|  Deployments                   ·                                          ·  |
·································|··············································
|  MyToken                       ·  —                                          |
·································|··············································

Hardhat的核心优势:

  • ►速度:测试执行时间缩短70%
  • ►调试:console.log()直接输出到终端
  • ►Gas报告:自动生成Gas消耗分析
  • ►分叉功能:可fork主网状态进行测试

速度是测试的生命线。慢一秒,漏洞就多一秒存活机会。

04 进阶测试策略:从单元测试到安全审计

基础测试只是起点。真正专业的测试工程师会构建多层防御体系。

第一层:单元测试(覆盖率>90%)

  • ►每个函数至少3个测试用例:正常路径、边界条件、异常路径
  • ►使用覆盖率工具检查遗漏
bash
# Hardhat覆盖率
npx hardhat coverage

第二层:集成测试(模拟真实场景)

  • ►多合约交互
  • ►时间依赖(如锁仓、时间锁)
  • ►外部预言机数据
javascript
// 时间相关测试示例
it("时间锁应阻止提前提取", async function () {
    const TimeLock = await ethers.getContractFactory("TimeLock");
    const timeLock = await TimeLock.deploy(7 * 24 * 60 * 60); // 7天锁仓
    
    await timeLock.deposit({ value: ethers.utils.parseEther("1") });
    
    await expect(
        timeLock.withdraw()
    ).to.be.revertedWith("Lock period not ended");
    
    // 模拟时间流逝
    await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
    await ethers.provider.send("evm_mine");
    
    await expect(timeLock.withdraw()).to.not.be.reverted;
});

第三层:安全测试(模拟攻击)

  • ►重入攻击测试
  • ►闪电贷攻击测试
  • ►权限提升测试
javascript
// 重入攻击测试
describe("重入攻击防御", function () {
    it("应阻止重入攻击", async function () {
        const VulnerableContract = await ethers.getContractFactory("VulnerableBank");
        const vulnerable = await VulnerableContract.deploy();
        
        const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
        const attacker = await Attacker.deploy(vulnerable.address);
        
        // 存入资金
        await vulnerable.deposit({ value: ethers.utils.parseEther("10") });
        await attacker.deposit({ value: ethers.utils.parseEther("1") });
        
        // 发起攻击
        await expect(
            attacker.attack({ value: ethers.utils.parseEther("1") })
        ).to.be.revertedWith("ReentrancyGuard: reentrant call");
    });
});

第四层:形式化验证(数学级保障)

  • ►使用Slither静态分析
  • ►使用MythX进行符号执行
  • ►使用Certora进行形式化验证
bash
# Slither静态分析
slither contracts/MyToken.sol

# MythX安全分析
npm install -g mythx-cli
mythx analyze contracts/MyToken.sol

第五层:Fuzz测试(随机输入轰炸)

  • ►使用Echidna进行属性测试
  • ►使用Foundry进行差异化测试
声明:本站部分稿件版权来源于网络,如有侵犯版权,请及时联系我们。

相关推荐

  • 测试管理平台对比:qTest vs SpiraTest vs PractiTest_千锋教育 01核心功能对决:谁在帮你“避坑”,谁在制造“新坑”测试管理平台不是Excel替代品,而是团队协作的“中枢神经”。功能差异,直接决定了测试流程是顺畅如水,还是处处卡顿。qTest:企业级“重器”,但学习曲线陡峭qTest由Tricentis
  • 自动化测试框架对比:Robot Framework vs Cypress vs Playwright_千锋教育 01三大框架的底层逻辑:谁在裸泳?测试框架的本质,不是“能不能跑”,而是“跑多快、修多快、活多久”。RobotFramework:老牌贵族。2005年出生,Python血统,关键字驱动。它像一个严格的管家,所有测试用例必须按他的规矩写。优势
  • 测试策略设计:从项目需求到测试计划的完整流程_千锋教育 01为什么90%的测试团队在需求阶段就已经失败?因为测试不是从代码开始的,是从需求文档的第一个字开始的。我见过太多团队:产品经理扔过来一份需求文档,测试人员扫一眼,直接打开Excel写测试用例。结果呢?上线当天,用户反馈的bug中,有47%
  • 无障碍测试自动化:使用axe-core进行自动化检测_千锋教育 01为什么你的网站正在失去30%的用户?全球15%的人口存在某种形式的残疾。在中国,超过1700万视障人士。他们不是“小众用户”,他们是真实存在的流量和收入来源。但现实是:90%的网站存在严重无障碍问题。WCAG(Web内容无障碍指南)定义
  • 区块链智能合约测试:Truffle与Hardhat实战_千锋教育 01为什么智能合约测试不是“锦上添花”,而是“生死存亡”2022年,跨链桥Ronin被黑,损失6.2亿美元。2023年,CurveFinance重入攻击,2.5亿美元蒸发。这些事故的共性?智能合约代码中的逻辑漏洞,在部署前未被充分测试。区块
  • 测试中的机器学习:使用TensorFlow进行异常检测_千锋教育 01为什么测试分析需要机器学习?从“看”到“判”的进化传统测试分析依赖规则引擎:写死阈值、定义关键词、设置正则表达式。这套方法在系统简单时管用,但一旦日志量膨胀到每天百万级,规则维护成本指数级上升。核心矛盾:规则是死的,异常是活的。&#96