学习Foundry之编写测试代码(一):初识Test合约
在没有接触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;
}
其中,Test
是Forge标准库
提供的合约,是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
的函数,分别是:deposit
、withdraw
和getBalance
。
首先,我们来测试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
合约编写对应的测试代码呢?
下期再会。