Forge 测试合约

本章讲解如何编写测试用例,以及如何使用 forge 对合约进行测试。

推特@Hita_DAO    DiscordHitaDAO

使用 Solidity 编写的智能合约,在部署到主网之前,都需要经过严格的测试。

我们可以在 Foundry 中使用 Solidity 语言编写测试用例,再运行 forge 命令对目标合约进行自动化测试。

被测试合约

默认方式下创建的 Foundry 项目,它的目录 src 中包含了一个被测试的例子合约 Counter.sol。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}

合约 Counter 包含两个函数:

  • 函数 setNumber 用来设置状态变量 number 的值。
  • 函数 increment 将状态变量 number 的值加 1。

测试用例合约

测试用例(Test Case)是软件测试中的基本概念。它是为了验证合约的某项功能是否按照需求正常工作,精心设计的一组输入数据、执行条件以及预期结果。

简单来说,测试用例就是一系列的步骤和检查点,用来帮助测试人员判断软件的特定部分是否正常工作。

Foundry 项目的测试用例中,如果测试的功能 revert,就表示测试失败;如果测试的结果符合预期,就表示测试通过。

默认方式下创建的 Foundry 项目,它的目录 test 中包含了一个测试用例合约 Counter.t.sol,专门用来测试 Counter.sol。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterTest is Test {
    Counter public counter;

    function setUp() public {
        counter = new Counter(); // 创建被测试合约 counter
        counter.setNumber(0);  // 调用被测试合约的函数 setNumber,设置初值
    }

    function test_Increment() public {
        counter.increment(); // 调用被测试合约的函数 increment
        assertEq(counter.number(), 1); // 判断被测试合约中 number 的值是否等于 1
    }

    function test_SetNumber(uint256 x) public {
        counter.setNumber(x); // 调用被测试合约的函数 setNumber
        assertEq(counter.number(), x); // 判断被测试合约中 number 的值是否等于 1
    }
}

测试合约需要继承 标准测试合约 Test,它定义在 forge-std/Test.sol 文件中,具有日志记录和断言功能。

我们需要在测试用例合约的开头通过 import 引入。

在这个测试用例合约 CounterTest 中,包含了 3 个函数:

  • setUp

它在每个测试用例运行之前被调用。这个函数通常用来创建要测试的合约,并做一些初始化工作。函数名称 setUp 是一个关键字,不能定义成其它名字,否则就不会被自动调用。

  • test_Increment

这是第一个测试用例,用来测试 Increment 函数的功能。测试用例的名字必须以关键字 test 开头。

先调用 counter 合约的 increment 函数,使得合约中的状态变量 number 加 1。

最后判断调用结果是否符合预期。其中,assertEq 是关键字,它是一个断言,用来判断两个参数是否相等,不相等就会 revert;两者相等则表示测试通过。

  • test_SetNumber

这是第二个测试用例,用来测试 SetNumber 函数的功能。测试用例的名字必须以关键字 test 开头。

先调用 counter 合约的 SetNumber 函数,设置合约中状态变量 number 的值为 x。

最后判断调用结果是否符合预期。

运行测试

我们编写测试用例后,就可以进行自动化测试了。

在命令行中输入测试命令:

forge test

输出测试结果:

[⠔] Compiling...
[⠒] Compiling 27 files with 0.8.13
[⠘] Solc 0.8.13 finished in 2.06s
Compiler run successful!

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] test_Increment() (gas: 31325)
[PASS] test_SetNumber(uint256) (runs: 256, μ: 30487, ~: 31265)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 24.39ms (16.52ms CPU time)

Ran 1 test suite in 313.80ms (24.39ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

我们可以看到输出的结果中:2 tests passed。这表示合约通过了测试。

如果我们修改一下测试用例函数:

    function test_Increment() public {
        counter.increment(); // 调用被测试合约的函数 increment
        assertEq(counter.number(), 100); // 判断被测试合约中 number 的值是否等于 100
    }

最后的断言语句,修改成判断运行结果是否等于100。 再次运行测试命令:

forge test

我们可以看到输出结果:

[⠰] Compiling...
[⠔] Compiling 1 files with 0.8.13
[⠒] Solc 0.8.13 finished in 2.04s
Compiler run successful!

Ran 2 tests for test/Counter.t.sol:CounterTest
[FAIL. Reason: assertion failed: 1 != 100] test_Increment() (gas: 31322)
[PASS] test_SetNumber(uint256) (runs: 256, μ: 30643, ~: 31265)
Suite result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 24.72ms (15.96ms CPU time)

Ran 1 test suite in 321.10ms (24.72ms CPU time): 1 tests passed, 1 failed, 0 skipped (2 total tests)

Failing tests:
Encountered 1 failing test in test/Counter.t.sol:CounterTest
[FAIL. Reason: assertion failed: 1 != 100] test_Increment() (gas: 31322)

Encountered a total of 1 failing tests, 1 tests succeeded

表示测试未通过,抛出了红色标记的异常。