🔍 第13课:测试与调试

DApp开发 ✅ 验证通过

🎯 学习目标:掌握Solidity测试策略(单元/集成/fuzz),理解Hardhat测试工具链,学会使用console.log调试。

📖 一、测试金字塔

/\ / \ Fuzz(随机输入发现边界bug) /Fuzz \ /--------\ / Integration \ 多合约交互 /----------------\ / Unit Tests \ 最快最基础 /--------------------\

单元测试是基础,覆盖所有函数的输入输出;集成测试验证多合约之间的交互逻辑;Fuzz测试使用随机输入发现边界条件bug。Solidity测试还包括不变量测试(Invariant Testing),验证系统状态始终满足某些约束条件。

💻 二、Hardhat测试进阶

const { expect } = require("chai");
const { loadFixture, time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

describe("Token", function () {
  async function deployFixture() {
    const [owner, addr1, addr2] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("Token");
    const token = await Token.deploy("Test","TST",1000000);
    return { token, owner, addr1, addr2 };
  }

  it("should transfer", async function () {
    const { token, owner, addr1 } = await loadFixture(deployFixture);
    await token.transfer(addr1.address, 100);
    expect(await token.balanceOf(addr1.address)).to.equal(100);
  });

  it("should fail insufficient balance", async function () {
    const { token, addr1, owner } = await loadFixture(deployFixture);
    await expect(token.connect(addr1).transfer(owner.address, 1))
      .to.be.revertedWith("Insufficient balance");
  });

  it("should emit Transfer event", async function () {
    const { token, owner, addr1 } = await loadFixture(deployFixture);
    await expect(token.transfer(addr1.address, 100))
      .to.emit(token, "Transfer")
      .withArgs(owner.address, addr1.address, 100);
  });

  it("should respect lock period", async function () {
    const { token, addr1 } = await loadFixture(deployFixture);
    await token.lock(addr1.address, 100, 3600);
    await expect(token.connect(addr1).withdraw(100))
      .to.be.revertedWith("Locked");
    await time.increase(3601);
    await expect(token.connect(addr1).withdraw(100)).to.not.be.reverted;
  });
});

📖 三、Solidity console.log调试

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "hardhat/console.sol";

contract DebugExample {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        console.log("Deposit from:", msg.sender);
        console.log("Old balance:", balances[msg.sender]);
        balances[msg.sender] += msg.value;
        console.log("New balance:", balances[msg.sender]);
        // ✅ 只在Hardhat网络输出,主网自动移除
    }
}
// ✅ 验证通过

📖 四、测试最佳实践

  1. 使用loadFixture避免重复部署
  2. 测试正常路径+边界条件+错误路径
  3. 验证事件emit和revert消息
  4. 测试访问控制(onlyOwner等)
  5. 测试时间依赖逻辑(time.increase)
  6. 使用npx hardhat coverage检查覆盖率
  7. Fuzz测试发现边界条件bug
  8. 测试Gas消耗是否合理
测试金字塔哪个最多? loadFixture? console.sol主网? time.increase? to.be.revertedWith?

🔧 动手实验

  1. 编写Token合约完整测试套件
  2. 使用console.log调试合约
  3. 测试时间依赖功能
  4. 运行npx hardhat coverage

📖 五、Fuzz Testing(模糊测试)

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

describe("Token Fuzz", function () {
  it("should never allow balance below zero", async function () {
    const [owner, addr1] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("Token");
    const token = await Token.deploy("T","T",1000000);

    // 随机金额测试
    for (let i = 0; i < 50; i++) {
      const amount = Math.floor(Math.random() * 1000);
      const balance = await token.balanceOf(owner.address);
      if (amount > balance) {
        await expect(token.transfer(addr1.address, amount))
          .to.be.reverted;
      }
    }
  });
});

📖 六、Gas优化测试

it("should report gas usage", async function () {
  const { token, addr1 } = await loadFixture(deployFixture);
  const tx = await token.transfer(addr1.address, 100);
  const receipt = await tx.wait();
  console.log("Transfer gas:", receipt.gasUsed.toString());
  expect(receipt.gasUsed).to.be.below(60000);
});

📖 七、常见陷阱与最佳实践

  1. ❌ 只测试正常路径不测试错误路径
  2. ❌ 不验证事件emit内容和参数
  3. ❌ 测试之间状态互相影响
  4. ❌ 忽略Gas消耗异常(可能暗示低效代码)
  5. ❌ 时间依赖逻辑不使用time.increase
  6. ❌ 跳过访问控制测试(onlyOwner等)
  7. ✅ 测试覆盖率目标: 90%+
  8. ✅ 使用Foundry进行Solidity原生fuzz测试

📖 八、Foundry替代方案

// Foundry: Solidity原生测试框架(比Hardhat快10x)
// 安装: curl -L https://foundry.paradigm.xyz | bash && foundryup

// test/Counter.t.sol
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/Counter.sol";

contract CounterTest is Test {
    Counter public counter;
    address public user = makeAddr("user");

    function setUp() public {
        counter = new Counter();
    }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.getCount(), 1);
    }

    function testFuzz_Add(uint256 x) public {
        // ✅ Foundry原生fuzz: 自动用随机值测试
        counter.add(x);
        assertEq(counter.getCount(), x);
    }

    function test_EmitEvent() public {
        vm.expectEmit(true, true, false, true);
        emit CountIncremented(1, address(this));
        counter.increment();
    }
}

📖 九、不变量测试(Invariant Testing)

// 不变量: 系统必须始终满足的条件
// 例: Token总供给 = 所有余额之和

// Foundry不变量测试
contract InvariantTest is Test {
    TokenHandler handler;
    Token token;

    function setUp() public {
        token = new Token("T","T",0);
        handler = new TokenHandler(token);
        targetContract(address(handler));
    }

    // ✅ 不变量: 总供给必须等于所有余额之和
    function invariant_totalSupplyEqualsBalances() public {
        uint256 total = token.totalSupply();
        uint256 sum = token.balanceOf(address(handler));
        assertEq(total, sum);
    }
}

📖 十、CI/CD自动化测试

// .github/workflows/test.yml
name: Hardhat Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npx hardhat compile
      - run: npx hardhat test
      - run: npx hardhat coverage
      - uses: codecov/codecov-action@v3
        with: { files: './coverage/lcov.info' }

📖 十一、调试技巧汇总

  1. console.log打印变量值(Hardhat网络专用)
  2. 使用hardhat-tracer追踪内部调用
  3. tenderly.co模拟和调试交易
  4. Remix IDE逐步调试
  5. etherscan读取已验证合约状态
  6. 使用foundry forge debug单步执行
  7. 比较预期和实际storage layout

📖 十二、安全测试专题

安全测试清单:
1. ✅ 重入攻击测试(外部调用+状态更新顺序)
2. ✅ 整数溢出测试(边界值: 0, max, max-1)
3. ✅ 访问控制测试(未授权用户应被拒绝)
4. ✅ 前端运行测试(FRB: 依赖block.timestamp)
5. ✅ 委托调用测试(delegatecall存储碰撞)
6. ✅ 拒绝服务测试(循环Gas耗尽)
7. ✅ 闪电贷攻击测试(价格操纵)
8. ✅ 签名重放测试(同一签名重复使用)

📖 十三、合约形式化验证

形式化验证 = 数学证明合约正确性:
• Certora Prover: 基于SMT求解器的形式化验证
• Halmos: 基于符号执行的Foundry测试
• Kontrol: KEVM形式化验证

不变量(Invariant)示例:
• totalSupply == sum(balances) — 代币守恒
• collateral >= borrowed * ratio — 超额抵押
• onlyOwner can call adminFn — 访问控制
• no reentrancy path — 重入自由

⚠️ 形式化验证成本高,但对高价值协议至关重要!

📖 十四、测试覆盖率标准

覆盖率类型目标说明
行覆盖率>90%每行代码至少执行一次
分支覆盖率>85%每个if/else分支都测试
函数覆盖率100%每个函数都调用
修饰符覆盖率100%每个modifier都验证

📖 十五、审计工具对比

自动化审计工具:
Slither: 静态分析,快速检测常见漏洞
Mythril: 符号执行,检测深层漏洞
Echidna: 属性测试(fuzz测试不变量)
Manticore: 多平台符号执行
Securify2: 基于模式的安全分析
4naly3er: 自动化审计报告生成

⚠️ 自动化工具不能替代人工审计,但可快速发现低级错误!

📖 十六、测试反模式

❌ 常见测试错误:
1. 测试只验证happy path(不测试错误场景)
2. 测试之间共享状态(用loadFixture隔离)
3. 硬编码地址和金额(用变量便于修改)
4. 不验证事件内容(只检查是否emit不够)
5. 不测试边界条件(0/max值/空数组)
6. 忽略Gas消耗变化(可能是bug信号)
7. 跳过权限测试(anyone可调用admin函数?)
8. 测试太慢(缺少并行或缓存)

本课全面覆盖了Solidity测试策略、Hardhat测试工具、console.log调试、Fuzz测试、Gas优化测试和常见测试反模式。

🔍

质量守护者

你已掌握合约测试——策略/console/覆盖率!

✅ 测试金字塔 ✅ Hardhat测试 ✅ console调试 ✅ 覆盖率

📋 课程目录