在没有接触Foundry之前,基本上都是通过Remix来对合约进行编译、部署、测试的。

本文来介绍一下,如何在Foundry工具创建的项目中针对合约代码来编写单元测试(测试代码)。

1 - 创建Foundry项目

我们以Solidity-by-examle中Re-Entrancy的代码为例,首先创建一个Foundry项目:

forge init Re-Entrancy

创建好项目后,将src/Counter.sol文件重命名为scr/Re-Entrancy.sol文件,并将test/Counter.t.sol重命名为test/Re-Entrancy.t.sol

cd Re-Entrancy
mv src/Counter.sol scr/Re-Entrancy.sol
mv test/Counter.t.sol test/Re-Entrancy.t.sol

2 - 替换代码内容

接下来,将scr/Re-Entrancy.sol文件的内容,修改为Re-Entrancy的示例代码。

3 - 编写测试代码

第一步,我们先删掉CounterTest的内容。

3-1 实现EnterStoreTest

接下来将import "../src/Counter.sol";修改为:import "../src/ReEntrancy.sol";

然后创建一个EtherStoreTest的合约继承于Test合约,代码如下:

contract EtherStoreTest is Test {
    EtherStore etherStore;
}

其中,TestForge标准库提供的合约,是DSTest的超集,是用forge写测试代码的首选。(在使用时,必须导入forge-std/Test.sol文件)

3-2 实现setUp函数

在执行testXXX函数之前,每次都会先执行setUp函数,所以我们需要在setUp函数中做一些初始化的操作。

接下来,我们需要在setUp函数中对etherStore状态变量进行初始化:

function setUp() public {
    etherStore = new EtherStore();
}

3-3 实现testDeposit函数

EtherStore合约里,一共有三个public的函数,分别是:depositwithdrawgetBalance

首先,我们来测试deposit函数,代码如下:

function testDeposit() public {
    assertEq(etherStore.getBalance(), 0);
    etherStore.deposit{value: 1 ether}();
    assertEq(etherStore.getBalance(), 1 ether);
}

首先,第一步,当etherStore初始化完成后,其balance值为0,我们通过asserEq函数来进行断言。

当我们调用deposit函数后,其balance会变成1 ether

接下来,我们尝试使用forge来对代码进行测试:

forge test -vvvv

[⠢] Compiling...
[⠒] Compiling 22 files with 0.8.18
[⠆] Solc 0.8.18 finished in 1.89s
Compiler run successful

Running 1 test for test/Counter.t.sol:EtherStoreTest
[PASS] testDeposit() (gas: 35768)
Traces:
  [35768] EtherStoreTest::testDeposit() 
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 0
    ├─ [22437] EtherStore::deposit{value: 1000000000000000000}() 
    │   └─ ← ()
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 1000000000000000000
    └─ ← ()

Test result: ok. 1 passed; 0 failed; finished in 268.88µs

完美!

3-4 实现testWithdraw函数

我们针对withdeaw函数进行测试时,也是一样的,先存入1 ether,再取出1 ether,最后期望getBalance函数返回0

function testWithdraw() public {
    assertEq(etherStore.getBalance(), 0);
    etherStore.deposit{value: 1 ether}();
    assertEq(etherStore.getBalance(), 1 ether);
    etherStore.withdraw();
    assertEq(etherStore.getBalance(), 0);
}

写完上述代码后,当我们满心欢喜运行forge test -vvvv时,却收到了报错:[FAIL. Reason: Failed to send Ether]

[⠢] Compiling...
[⠊] Compiling 1 files with 0.8.18
[⠒] Solc 0.8.18 finished in 654.52ms
Compiler run successful

Running 2 tests for test/Counter.t.sol:EtherStoreTest
[PASS] testDeposit() (gas: 35768)
Traces:
  [35768] EtherStoreTest::testDeposit() 
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 0
    ├─ [22437] EtherStore::deposit{value: 1000000000000000000}() 
    │   └─ ← ()
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 1000000000000000000
    └─ ← ()

[FAIL. Reason: Failed to send Ether] testWithdraw() (gas: 43625)
Traces:
  [164494] EtherStoreTest::setUp() 
    ├─ [109959] → new EtherStore@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    │   └─ ← 549 bytes of code
    └─ ← ()

  [43625] EtherStoreTest::testWithdraw() 
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 0
    ├─ [22437] EtherStore::deposit{value: 1000000000000000000}() 
    │   └─ ← ()
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 1000000000000000000
    ├─ [7418] EtherStore::withdraw() 
    │   ├─ [45] EtherStoreTest::fallback{value: 1000000000000000000}() 
    │   │   └─ ← "EvmError: Revert"
    │   └─ ← "Failed to send Ether"
    └─ ← "Failed to send Ether"

Test result: FAILED. 1 passed; 1 failed; finished in 329.17µs

Failing tests:
Encountered 1 failing test in test/Counter.t.sol:EtherStoreTest
[FAIL. Reason: Failed to send Ether] testWithdraw() (gas: 43625)

Encountered a total of 1 failing tests, 1 tests succeeded

这是因为我们的EtherStoreTest合约即没有实现receive函数也没有实现fallback函数,导致EtherStore合约向EtherStoreTest合约转账时出错了。

所以,解决方案也很简单,新增一个receive函数即可:

receive() external payable{}

最后再来执行forge test -vvvv,结果:

[⠢] Compiling...
[⠃] Compiling 1 files with 0.8.18
[⠒] Solc 0.8.18 finished in 648.37ms
Compiler run successful

Running 2 tests for test/Counter.t.sol:EtherStoreTest
[PASS] testDeposit() (gas: 35768)
Traces:
  [35768] EtherStoreTest::testDeposit() 
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 0
    ├─ [22437] EtherStore::deposit{value: 1000000000000000000}() 
    │   └─ ← ()
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 1000000000000000000
    └─ ← ()

[PASS] testWithdraw() (gas: 31305)
Traces:
  [35518] EtherStoreTest::testWithdraw() 
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 0
    ├─ [22437] EtherStore::deposit{value: 1000000000000000000}() 
    │   └─ ← ()
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 1000000000000000000
    ├─ [6004] EtherStore::withdraw() 
    │   ├─ [55] EtherStoreTest::receive{value: 1000000000000000000}() 
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [149] EtherStore::getBalance() [staticcall]
    │   └─ ← 0
    └─ ← ()

Test result: ok. 2 passed; 0 failed; finished in 374.25µs

完美!!!

那么,问题来了,如何针对Attack合约编写对应的测试代码呢?

下期再会。