在没有接触Foundry之前,基本上都是通过Remix来对合约进行编译、部署、测试的。
本文来介绍一下,如何在Foundry工具创建的项目中针对合约代码来编写单元测试(测试代码)。
1 - 创建Foundry项目
我们以Solidity-by-examle中Re-Entrancy的代码为例,首先创建一个Foundry项目:
创建好项目后,将src/Counter.sol文件重命名为scr/Re-Entrancy.sol文件,并将test/Counter.t.sol重命名为test/Re-Entrancy.t.sol
1 2 3
| 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合约,代码如下:
1 2 3
| contract EtherStoreTest is Test { EtherStore etherStore; }
|
其中,Test是Forge标准库提供的合约,是DSTest的超集,是用forge写测试代码的首选。(在使用时,必须导入forge-std/Test.sol文件)
3-2 实现setUp函数
在执行testXXX函数之前,每次都会先执行setUp函数,所以我们需要在setUp函数中做一些初始化的操作。
接下来,我们需要在setUp函数中对etherStore状态变量进行初始化:
1 2 3
| function setUp() public { etherStore = new EtherStore(); }
|
3-3 实现testDeposit函数
在EtherStore合约里,一共有三个public的函数,分别是:deposit、withdraw和getBalance。
首先,我们来测试deposit函数,代码如下:
1 2 3 4 5
| 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来对代码进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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
1 2 3 4 5 6 7
| 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]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| [⠢] 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函数即可:
1
| receive() external payable{}
|
最后再来执行forge test -vvvv,结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| [⠢] 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合约编写对应的测试代码呢?
下期再会。