手摸手教你如何使用foundry进行测试脚本编写
Published: 2023-07-14 01:53:47


## 什么是foundry?
foundry是一个solidity智能合约开发工具。可以帮你管理依赖包，编译项目，运行测试脚本，还可以让你通过命令行工具或者script脚本和链上合约进行交互。和hardhat不同的地方是，hardhat我们还是主要用来开发大型的合约项目，但是foundry用来进行编写测试脚本我认为是非常方便的。主要的特点是可以直接使用solidity编写合约测试脚本，无需在JavaScript和solidity之间进行切换。虽然在hardhat也可以编写测试脚本，但是有时候需要切换语言或者对类型进行转换相对会比较麻烦。

## 安装和配置
Foundryup是foundry的安装器。我们可以直接通过终端命令行进行安装:

```shell
curl -L https://foundry.paradigm.xyz | bash
```

或者要是你喜欢也可以通过源码的方式进行编译安装

```shell
# clone the repository
git clone https://github.com/foundry-rs/foundry.git
cd foundry
# install Forge + Cast
cargo install --path ./cli --profile local --bins --force
# install Anvil
cargo install --path ./anvil --profile local --force
# install Chisel
cargo install --path ./chisel --profile local --force

```

## 初始化
我们可以通过forge命令行直接初始化新项目

```shell
forge init helle_foundry
```

初始化之后的大概是这么个目录结构:
```shell
$ cd hello_foundry
$ tree . -d -L 1
.
├── lib     依赖的包文件目录
├── script  自定义的脚本目录
├── src     合约源码目录
└── test    测试脚本目录

```

## 特性和使用

### 编写测试脚本
测试脚本的基本代码结构：
```solidity
pragma solidity 0.8.10;

import "forge-std/Test.sol";

contract ContractBTest is Test {
    uint256 testNumber;

    function setUp() public {
        testNumber = 42;
    }

    function test_NumberIs42() public {
        assertEq(testNumber, 42);
    }

    function testFail_Subtract43() public {
        testNumber -= 43;
    }
}

```
在上面的例子中每个test函数都是相互独立的，setUp函数类似于我们的构造函数，会首先进行调用setUp函数进行初始化。在我们实际的操作中，可以直接导入需要测试的合约，然后在setUp中对合约进行初始化。我们只需要一个变量就能调用目标合约中的函数进行测试。

运行测试脚本的命令:
```shell 
forge test --match-contract "ContractBTest(合约名称)" -vvv
```

### debug 
我们可以通过`-vvv` 和 `-vvvv`进行数据跟踪。在打印出来的日志中，我们通过颜色来区分不同的错误类型(如果你的终端支持颜色显示的话)：

* 绿色 : 正常的调用
* 红色 : 回滚的调用
* 蓝色 : 作弊码的调用
* 青色 : 日志
* 黄色 : 合约部署

```shell
  [24661] OwnerUpOnlyTest::testIncrementAsOwner()
    ├─ [2262] OwnerUpOnly::count()
    │   └─ ← 0
    ├─ [20398] OwnerUpOnly::increment()
    │   └─ ← ()
    ├─ [262] OwnerUpOnly::count()
    │   └─ ← 1
    └─ ← ()

```
### fuzz测试
```solidity
contract SafeTest is Test {

    function testFuzz_Withdraw(uint256 amount) public {
        payable(address(safe)).transfer(amount);
        uint256 preBalance = address(this).balance;
        safe.withdraw();
        uint256 postBalance = address(this).balance;
        assertEq(preBalance + amount, postBalance);
    }
}

```
比如上面的提款测试，普通的test测试函数需要自定义一个固定的amount金额，但是这个指定的amount不能覆盖到所有的情景，另一个选择就是使用testFuzz.fuzz测试的函数名和普通测试对比只是多了fuzz，需要注意的是所有的测试函数需要用`test`或者`testFuzz`开头否则是不会被认为是一个测试函数的。

### console.log日志打印
在solidity中打印日志需要提前对日志内容的类型进行定义。foundry支持下面的一些数据类型进行日志打印，可以帮助我们进行debug:

* console.logInt(int i)
* console.logUint(uint i)
* console.logString(string memory s)
* console.logBool(bool b)
* console.logAddress(address a)
* console.logBytes(bytes memory b)
* console.logBytes1(bytes1 b)
* console.logBytes2(bytes2 b)
* console.logBytes32(bytes32 b)
对于一些更加复杂的数据结构，比如我们的目标合约中返回了一个struct类型的数据。我们可以直接使用targetContract.StructName对数据进行定义。

### assert
foundry中主要有3种类型的assert
* assert(conditon) 判断一个bool值
* assertEq(a,b)    判断a，b两个值是否相等
* assertTrue(condition,"reason")
```solidity
function testGetTotalShares() public {
	assertEq(prizePool.getTotalShares(), 220);
}
```

### 时间和区块进行改变
某些合约中可能有时间间隔，比如一个stake asset操作之后，可能需要一个月的时间间隔之后才能进行下一次操作，这个时候我们可以直接去修改evm中的时间
```shell
vm.warp(block.timstamp + 30 days);
```
或者我们也可以对区块进行增加
```shell
vm.roll(block.number + 1);
```

### vm.expectRevert
在某些测试中，对于条件不满足的情况下，合约逻辑中可能会存在一些revert的情况，我们需要对这些revert进行捕捉的时候可以用到`vm.expertRevert`

* expectRevert() 
* expectRevert(bytes4 message)
* expectRevert(bytes calldata message)

```solidity
function testLowLevelCallRevert() public {
    vm.expectRevert(bytes("error message"));
    (bool revertsAsExpected, ) = address(myContract).call(myCalldata);
    assertTrue(revertsAsExpected, "expectRevert: call did not revert");
}
```
有些合约对一些多次出现的错误可能把它定义为一个error类型，这个时候我们可以通过`CustomError.selector`对错误进行捕捉。
```shell
vm.expectRevert(CustomError.selector);
```

```shell
vm.expectRevert(
    abi.encodeWithSelector(CustomError.selector, 1, 2)
);

```

对于那些直接revert并没有定义错误的情况下:
```shell
 vm.expectRevert(bytes(""));
```

follow我的推进行交流学习:https://twitter.com/nifty12111
TG:@coffiasd