---
# System prepended metadata

title: Foundry NFT實作教學/介紹 (3)Unit Test

---

# Foundry NFT實作教學/介紹 (3)Unit Test

# 前言

上篇: [Foundry NFT實作教學/介紹 (2)開發並部署合約](https://hackmd.io/@RainFox/B1UlEhQzn)

到上篇為止，我們已經完成開發並部署，同時也用了anvil cast的部分功能，這篇我們就會介紹Foundry最受他人喜歡的功能，**Test**

對於Test的功能本人我也還沒完全探索完成，因此這篇文章算是拋磚引玉之用，期待有更多人能夠分享相關的使用方法，若想仔細了解其結構推薦閱讀官方文檔

# 開始

### 1. 建立測試檔案

先在test資料夾中創建`NFT.t.sol`這個檔案，並將以下的程式碼複製進去
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/NFT.sol";

contract NFTTest is Test{

}

```

上面import的部分除了原本的contract NFT以外，還需要多import一個Test.sol，Test當中包含很多提供測試用的function，晚點我們一一介紹


### 2. 寫測試

接下來我們就開始在`NFTTest`這個合約裡面寫測試案例

首先我們必須先宣告一個`function setUp() external {}`，這個`setUp`的funciton是Foundry的規定，作用跟`beforeEach`相同，每執行一個test就會執行`setUp`



```solidity

contract NFTTest is Test{
    NFT nft;
    
    function setUp() external {
        nft = new NFT(); //每執行一個test，我們就去建立一個新的contract instance

    }
}

```

接著寫我們的第一個test，讓這個test確認symbol()跟name()是否符合我們預期

```solidity
function testNameAndSymbol() external{
    assertEq(nft.name(), "NFT");
    assertEq(nft.symbol(), "NFT");
}
```
> 這邊test function 都需要宣告為public或external供外部調用，且function 的名字都需要以`test`做開頭

### 3. 執行測試

輸入以下指令

```
forge test
```

Output:
```
[⠔] Compiling...
[⠘] Compiling 1 files with 0.8.18
[⠃] Solc 0.8.18 finished in 1.26s
Compiler run successful

Running 1 test for test/NFT.t.sol:NFTTest
[PASS] testInfo() (gas: 14573)
Test result: ok. 1 passed; 0 failed; finished in 3.51ms

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testIncrement() (gas: 28334)
[PASS] testSetNumber(uint256) (runs: 256, μ: 27553, ~: 28409)
Test result: ok. 2 passed; 0 failed; finished in 12.33ms
```

你會發現，除了NFT.t.sol執行測試以外，預設的Counter.t.sol也執行了測試(如果你沒有把他刪掉的話)

因此若要指定執行的測試合約，需要將指令改為

```
forge test --match-contract NFTTest
```




如此一來就只會執行你指定的測試合約去執行了

到目前為止，整個test的流程算是差不多了，接下來就會介紹幾個test常用的功能

# Cheatcode 

在test裡面，我們常常需要去做一些外部狀態的更改，例如更換別人來調用function，偵測event等等，這時我們就會需要cheatcode，這邊我們用測試mint來做示範

```solidity
function testMint() external {
    //arrange
    uint256 userPrivateKey = vm.envUint("USER_PRIVATE_KEY"); // 取得.env檔案中的USER_PRIVATE_KEY
    address user = vm.addr(userPrivateKey); //將得到private key轉化為address，以便等等當作參數使用
    vm.broadcast(userPrivateKey); // 宣告下一個transaction的sender是誰，這邊我放入user的private key
    
    //act
    nft.mint(user, 1); //執行mint，因為剛剛有使用broadcast，所以這邊transaction的sender是user
    
    
    //assert
    assertEq(nft.ownerOf(1), user);
    assertEq(nft.balanceOf(user), 1);
}
```

當中的`envUint()` `addr()` `broadcast()`都是一個cheatcode，並且存放在vm這個合約中，若想要知道有什麼語法可以參考 [Cheatcodes Reference](https://book.getfoundry.sh/cheatcodes/) 以及 [Ceatcodes](https://book.getfoundry.sh/forge/cheatcodes)

接著我們再做進一步的測試，我們需要去偵測event有沒有被觸發

在ERC721中執行`_mint`時，會觸發一個`Tranfer(address from, address to, uint256 tokenId)`的event

```solidity=
function testMint() external {
    //arrange
    uint256 userPrivateKey = vm.envUint("USER_PRIVATE_KEY"); // 取得.env檔案中的USER_PRIVATE_KEY
    address user = vm.addr(userPrivateKey); //將得到private key轉化為address，以便等等當作參數使用
    vm.broadcast(userPrivateKey); // 宣告下一個transaction的sender是誰，這邊我放入user的private key
    //新增event偵測
    vm.expectEmit(true, true, true, true);
    //指定要偵測的event
    emit Transfer(address(0), user, 1);
    
    //act
    nft.mint(user, 1); //執行mint，因為剛剛有使用broadcast，所以這邊transaction的sender是user
    
    
    //assert
    assertEq(nft.ownerOf(1), user);
    assertEq(nft.balanceOf(user), 1);
}
```

`vm.expectEmit(true, true, true, true)`，前三個true是設定前三個event 中`indexed`的參數需不需要去做比對，最後一個true則是設定除了三個`indexed`以外的參數需不需要去做比對

我們可以做個測試來更明白這個功能，將test event的部分改為
```solidity=
vm.expectEmit(true, true, true, true);

//更改Transfer tokenId的參數部分
emit Transfer(address(0), user, 100)
```

接著去跑測試，理論上結果應該不會過，因為你實際mint的tokenId是1不是100

測試結果:
```
[⠘] Compiling...
[⠔] Compiling 1 files with 0.8.18
[⠒] Solc 0.8.18 finished in 1.66s
Compiler run successful

Running 2 tests for test/NFT.t.sol:NFTTest
[PASS] testInfo() (gas: 14596)
[FAIL. Reason: Log != expected log] testMint() (gas: 60682)
Test result: FAILED. 1 passed; 1 failed; finished in 5.57ms

Failing tests:
Encountered 1 failing test in test/NFT.t.sol:NFTTest
[FAIL. Reason: Log != expected log] testMint() (gas: 60682)
```
但是這時候若我們把`vm.expectEmit(true, true, true, true)`改為`vm.expectEmit(true, true, false, true)`，**意指第三個`indexed`參數不需要去做比對**

再跑一次測試，這時候會發現測試就會過了，即便你mint的tokenId不是100而是1
```
[⠑] Compiling...
[⠔] Compiling 1 files with 0.8.18
[⠑] Solc 0.8.18 finished in 1.83s
Compiler run successful

Running 2 tests for test/NFT.t.sol:NFTTest
[PASS] testInfo() (gas: 14596)
[PASS] testMint() (gas: 60679)
Test result: ok. 2 passed; 0 failed; finished in 5.39ms
```

# Trace code 

在上面的test中，你可以選擇讓test顯示出更多的資訊，使用`-vvvv`

```
forge test -vvvv --match-contract NFTTest 
```

Output:
```
[⠒] Compiling...
No files changed, compilation skipped

Running 2 tests for test/NFT.t.sol:NFTTest
[PASS] testInfo() (gas: 14596)
Traces:
  [14596] NFTTest::testInfo()
    ├─ [3285] NFT::name() [staticcall]
    │   └─ ← NFT
    ├─ [3306] NFT::symbol() [staticcall]
    │   └─ ← NFT
    └─ ← ()

[PASS] testMint() (gas: 60682)
Traces:
  [60682] NFTTest::testMint()
    ├─ [0] VM::envUint(USER_PRIVATE_KEY) [staticcall]
    │   └─ ← <env var value>
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
    ├─ [0] VM::expectEmit(true, true, true, true)
    │   └─ ← ()
    ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8, tokenId: 1)
    ├─ [0] VM::broadcast(<pk>)
    │   └─ ← ()
    ├─ [47186] NFT::mint(0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 1)
    │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8, tokenId: 1)
    │   └─ ← ()
    ├─ [536] NFT::ownerOf(1) [staticcall]
    │   └─ ← 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
    ├─ [634] NFT::balanceOf(0x70997970C51812dc3A010C7d01b50e0d17dc79C8) [staticcall]
    │   └─ ← 1
    └─ ← ()

Test result: ok. 2 passed; 0 failed; finished in 5.80ms
```

這邊可以看到他顯示，他call了哪些function，參數是什麼，以及return什麼，如果有錯誤的話，他會告訴你錯誤是發生在哪裡

剛剛我們採用的是四個v的模式(-vvvv)，他也有多種模式可以選擇，從-vv 到-vvvvv
，越多v代表會顯示越詳細的訊息

官方說明 [forge-test](https://book.getfoundry.sh/reference/forge/forge-test)
```
-v
--verbosity
    Verbosity of the EVM.

    Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv).

    Verbosity levels:
    - 2: Print logs for all tests
    - 3: Print execution traces for failing tests
    - 4: Print execution traces for all tests, and setup traces for failing tests
    - 5: Print execution and setup traces for all tests
```

知道了這些後，我們可以就可以用`console.log`來顯示某些資訊了

```solidity
function testMint() external {
    uint256 userPrivateKey = vm.envUint("USER_PRIVATE_KEY");
    address user = vm.addr(userPrivateKey);
    //Add console.log
    console.log(user);
    vm.expectEmit(true, true, true, true);
    emit Transfer(address(0), user, 1);

    vm.broadcast(userPrivateKey);
    nft.mint(user, 1);
    assertEq(nft.ownerOf(1), user);
    assertEq(nft.balanceOf(user), 1);
}
```

如果這邊在做test不指定v(verbosity)，那麼`console.log`的資訊是不會顯示出來的，因此我們這邊將test的command改為

```
forge test -vv --match-contract NFTTest
```

Output:
```
[⠔] Compiling...
No files changed, compilation skipped

Running 2 tests for test/NFT.t.sol:NFTTest
[PASS] testInfo() (gas: 14596)
[PASS] testMint() (gas: 63524)
Logs:
  0x70997970C51812dc3A010C7d01b50e0d17dc79C8

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

可以看到我們在Logs的欄位顯示出user這個address


# Fuzz test

Fuzz test個人認為是一個還蠻有趣的功能，簡單來說，就是將要測試的參數都用亂數去測，來檢測這個function是不是在各種情況下都能正常運作

實際使用的方式是在test function當中傳入參數，讓我們將`function testMint()`改為`function testMint(uint256 tokenId)`，同時也需要把測試當中原本tokenId的部分都從1改為tokenId，具體程式碼如下：

```solidity
function testMint(uint256 tokenId) external {
    //arrange
    uint256 userPrivateKey = vm.envUint("USER_PRIVATE_KEY");
    address user = vm.addr(userPrivateKey);
    console.log(user);
    vm.expectEmit(true, true, true, true);
    emit Transfer(address(0), user, tokenId); //1 --> tokenId
    vm.broadcast(userPrivateKey);

    //act
    nft.mint(user, tokenId); //1 --> tokenId

    //assert
    assertEq(nft.ownerOf(tokenId), user); //1 --> tokenId
    assertEq(nft.balanceOf(user), 1);
}
```

接著執行測試，理論上應該會過，並且會發現後面多了(runs: 256, μ: 63587, ~: 63587)，數字略有不同的地方是正常的

- runs是代表這個test經過多少次fuzz test

- "μ"代表所有測試花費gas的平均數(mean)

- "~"代表所有測試花費gas的中位數(median)

我們再稍微改一下合約來更了解fuzz test

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";

contract NFT is ERC721 {

    uint256 public totalSupply = 100;

    constructor() ERC721("NFT", "NFT") {}

    function mint(address to, uint256 tokenId) public {
        require(tokenId <= totalSupply, "NFT: tokenId is out of range");
        _mint(to, tokenId);
    }
}

```

我們新增了totalSupply，並且設定require，tokenId必須小於totalSupply

接著再執行一次test

Output:
```
[⠔] Compiling...
[⠒] Compiling 2 files with 0.8.18
[⠆] Solc 0.8.18 finished in 1.52s
Compiler run successful

Running 2 tests for test/NFT.t.sol:NFTTest
[PASS] testInfo() (gas: 14640)
[FAIL. Reason: Log != expected log Counterexample: calldata=0x2f3f94ae0000000000000000000000000000000000000000000000000000000000000065, args=[101]] testMint(uint256) (runs: 4, μ: 65779, ~: 65779)
Test result: FAILED. 1 passed; 1 failed; finished in 55.12ms

Failing tests:
Encountered 1 failing test in test/NFT.t.sol:NFTTest
[FAIL. Reason: Log != expected log Counterexample: calldata=0x2f3f94ae0000000000000000000000000000000000000000000000000000000000000065, args=[101]] testMint(uint256) (runs: 4, μ: 65779, ~: 65779)

Encountered a total of 1 failing tests, 1 tests succeeded
```

會發現test沒辦法過了，而且還可以知道，**是當你的tokenId是101的時候發生錯誤的**

那麼問題來了，若我希望傳入的tokenId不要超過totalSupply的話，應該怎麼做？

這邊採用`assume`來規定fuzz test參數的範圍
```solidity
function testMint(uint256 tokenId) external {
    //arrange
    uint256 totalSupply = nft.totalSupply();
    //設定vm.assume，括號當中的condition必須為true才會執行後面的test
    vm.assume(tokenId <= totalSupply);
    
    uint256 userPrivateKey = vm.envUint("USER_PRIVATE_KEY");
    address user = vm.addr(userPrivateKey);
    console.log(user);
    vm.expectEmit(true, true, true, true);
    emit Transfer(address(0), user, tokenId);
    vm.broadcast(userPrivateKey);

    //act
    nft.mint(user, tokenId);

    //assert
    assertEq(nft.ownerOf(tokenId), user);
    assertEq(nft.balanceOf(user), 1);
}
```

接著這邊再去做測試，就會發現測試都可以通過了

詳細內容可以參考官方文件[Fuzz test](https://book.getfoundry.sh/forge/fuzz-testing)

# Revert test

接下來我們需要測試，當錯誤情況發生時，會不會revert

有兩種方法可以採用

1. 將function的名稱設為`testFail`開頭
2. 程式碼中加入`vm.expectRevert()`來確認revert message是不是跟所想的一樣

我們這時候另寫一個function來測試當tokenId大於totalSupply時，會不會都revert

```solidity
function testFail_WhenTokenIdGreaterThanTotalSupply(uint256 tokenId) external{
    //arrange
    uint256 totalSupply = nft.totalSupply();
    //將tokenId的條件都設為必須大於totalSupply
    vm.assume(tokenId > totalSupply);

    uint256 userPrivateKey = vm.envUint("USER_PRIVATE_KEY");
    address user = vm.addr(userPrivateKey);
    vm.broadcast(userPrivateKey);

    //act
    nft.mint(user, tokenId);
}
```

接著執行測試，並發現所有測試都通過了

但是這樣的測試可能不夠準確，畢竟我們這個測試的目的是要測試`mint()`當中`require`有沒有發揮作用，但是testFail只能確定`nft.mint()`是失敗的，但卻不能確定是因為何種原因而失敗

因此這邊我們可以用第二種方法，來比對revert message是否確實是我們想要的

```solidity

function testRevert_WhenTokenIdGreaterThanTotalSupply(uint256 tokenId) external{
    //arrange
    uint256 totalSupply = nft.totalSupply();
    vm.assume(tokenId > totalSupply);
    
    uint256 userPrivateKey = vm.envUint("USER_PRIVATE_KEY");
    address user = vm.addr(userPrivateKey);
    vm.broadcast(userPrivateKey);

    //assert
    //這邊預期Revert的message為NFT: tokenId is out of range
    vm.expectRevert("NFT: tokenId is out of range");

    //act
    nft.mint(user, tokenId);
}
```
> 若用這種方法，function的名稱除了需要以test為開頭外，並無特別的要求



# 結尾

到這裡為止，簡單的Foundry入門就完成了

此系列文章還有許多功能還沒提到，例如: Invariant test、hardhat兼容等等，有部分的功能我也還沒探索

目前用完的感想是，Foundry的想法很新且有趣，對test功能部分有特別加強，整體的語言都用solidity寫，寫起來會較為順暢，不需要去處理跨語言之間的型別轉換問題

但考量到整體環境ethers還是有相當重要的地位，且現在在智能合約開發上了解ethers也是必須的，因此熟悉hardhat的開發者我認為沒有迫切的必要遷移至Foundry，但是值得了解一下

後續若我有繼續探索Foundry也會繼續寫成文章分享出來，若其他人有其他建議跟分享，也歡迎告訴我

若在製作上有問題，可以參考原始碼: [hello-foundry](https://github.com/LI-YONG-QI/hello-foundry)

























