首頁 > 軟體

Hardhat進行合約測試環境準備及方法詳解

2023-03-09 06:04:50

引言

Hardhat是一個開源的以太坊開發框架,簡單好用、可延伸、可客製化的特點讓它在開發者中間很受歡迎。Hardhat在支援編輯、編譯、偵錯和部署合約方面都非常的方便,也有很多功能可以使合約測試工作更加高效和便捷,本文就是聚焦在合約測試領域,探尋Hardhat的特點和日常測試過程中的一些使用技巧。

一、環境準備

可以參考Hardhat官網教學,進行環境的準備和Hardhat安裝。

Hardhat提供了快速構建合約工程的方法:

  • 建立空的工程目錄
  • 在目錄下執行npx hardhat
  • 根據互動提示完成Hardhat工程的建立

二、範例合約與測試方法

快速建立Hardhat工程,可以在contract目錄下看到Lock.sol的合約,此合約是一個簡單的範例,實現了在指定時間前(unlockTime)鎖定資產的功能。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Uncomment this line to use console.log
import "hardhat/console.sol";
contract Lock {
    uint public unlockTime;
    address payable public owner;
    event Withdrawal(uint amount, uint when);
    constructor(uint _unlockTime) payable {
        require(
            block.timestamp < _unlockTime,
            "Unlock time should be in the future"
        );
        unlockTime = _unlockTime;
        owner = payable(msg.sender);
    }
    function withdraw() public {
        // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
        console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);
        require(block.timestamp >= unlockTime, "You can't withdraw yet");
        require(msg.sender == owner, "You aren't the owner");
        emit Withdrawal(address(this).balance, block.timestamp);
        owner.transfer(address(this).balance);
    }
}

同時,在test目錄下,有Lock.ts(或Lock.js)的測試程式碼,測試程式碼裡分別展示了對合約部署,合約中涉及的功能的測試。其中值得學習的部分:

一是定義了一個具有setup功能的函數,此函數定義了一些狀態變數的初始狀態,後面在每次測試程式碼執行前,可以通過loadFixture方法執行此函數,把狀態變數還原到函數中定義的初始狀態。這種給狀態變數取快照,並用快照還原的方式,解決了很多因為狀態變數改變而測試用例執行異常的問題,是個很有用很便捷的方法。

另一個是用到了很便捷的斷言方式,這就省掉了寫很多麻煩的校驗條件,來驗證一個執行結果。比如下面這個斷言,直接能驗證當withdraw函數被呼叫後出現的回滾情況:

await expect(lock.withdraw()).to.be.revertedWith( "You can't withdraw yet" );

三、LoadFixture的使用

使用場景

用於每次執行測試前的setup操作,可以定義一個函數,在此函數中完成諸如合約部署,合約初始化,賬戶初始化等操作,在每次執行測試前利用loadFixture的功能,進行相同的變數狀態的設定,對合約測試提供了很大的幫助。

工作原理

根據Hardhat原始碼,可以看到loadFixture維護了一個快照陣列snapshots,一個快照元素包含:

不同的函數f作為loadFixture入參時,會有不同的snapshot儲存在loadFixture維護的snapshots陣列中。

在loadFixture(f)首次執行時,屬於f函數的snapshot為undefined,此時會記錄f函數中定義的全部狀態變數,同時執行:

const restorer = await takeSnapshot();

並將此時的snapshot元素加入到snapshots陣列中,後面再次用到同一個入參函數f的loadFixture時,在快照陣列snapshots中已存在快照,可直接進行區塊鏈狀態回滾: await snapshot.restorer.restore();

  • fixture: Fixture型別的入參函數,type Fixture = () => Promise;
  • data:fixture函數中定義的狀態變數
  • restorer:一個有restore方法的結構體,在“./helpers/takeSnapshot”方法中有定義,可以觸發evm_revert操作,指定區塊鏈退回到某個快照點。

loadFixture的用法

官方檔案範例如下:

```js
async function deployContractsFixture() {
  const token = await Token.deploy(...);
  const exchange = await Exchange.deploy(...);
  return { token, exchange };
}
it("test", async function () {
  const { token, exchange } = await loadFixture(deployContractsFixture);
  // use token and exchanges contracts
})
```
注意:loadFixture的入參不可以是匿名函數,即:
```js
//錯誤寫法 
loadFixture(async () => { ... }) 
//正確寫法 
async function beforeTest(){ 
//定義函數 
} 
loadFixture(beforeTest);
```

四、Matchers的使用

Machers:在chai斷言庫的基礎上增加了以太坊特色的斷言,便於測試使用

1.Events用法

   contract Ademo {
    event Event();
    function callFunction () public {
        emit Event();
    }
}

對合約C的call方法進行呼叫會觸發一個無引數事件,為了測試這個事件是否被觸發,可以直接用hardhat-chai-matchers中的Events斷言,用法如下:

    const A=await ethers.getContractFactory("Ademo");
    const a=await A.deploy();
    //採用hardhat-chai-matchers的斷言方式,判斷Events是否觸發
    await expect(a.callFunction()).to.emit(a,"Event");
  • Reverts用法:
    //最簡單的判斷revert的方式
await expect(contract.call()).to.be.reverted;
//判斷未發生revert
await expect(contract.call()).not.to.be.reverted;
//判斷revert發生並且帶了指定的錯誤資訊
await expect(contract.call()).to.be.revertedWith("Some revert message");
//判斷未發生revert並且攜帶指定資訊
await expect(contract.call()).not.to.be.revertedWith("Another revert message");

除了上述常用的判斷場景外,hardhat-chai-matchers還支援了對Panic以及客製化化Error的判定:

await expect(…).to.be.revertedWithPanic(PANIC_CODES)
await expect(…).not.to.be.revertedWithPanic(PANIC_CODES)
await expect(…).to.be.revertedWithCustomError(CONTRACT,"CustomErrorName")
await expect(…).to.be.revertedWithoutReason();
  • Big Number

在solidity中最大整型數是2^256,而JavaScript中的最大安全數是2^53-1,如果用JS寫solidity合約中返回的大數的斷言,就會出現問題。hardhat-chai-matchers提供了關於大數的斷言能力,使用者無需關心大數之間比較的關係,直接以數位的形式使用即可,比如: expect(await token.balanceOf(someAddress)).to.equal(1);

關於JavaScript的最大安全數問題:

Number.MAX_SAFE_INTEGER 常數表示在 JavaScript 中最大的安全整數,其值為2^53-1,即9007199254740991 。因為Javascript的數位儲存使用了IEEE 754中規定的雙精度浮點數資料型別,而這一資料型別能夠安全儲存(-2^53-1 ~ 2^53-1)之間的數(包括邊界值),超出範圍後將會出現錯誤,比如:

const x = Number.MAX_SAFE_INTEGER + 1;
const y = Number.MAX_SAFE_INTEGER + 2;
console.log(Number.MAX_SAFE_INTEGER);
// Expected output: 9007199254740991
console.log(x);
console.log(y);
// Expected output: 9007199254740992
console.log(x === y);
// Expected output: true

Balance Changes

可以很方便的檢測使用者錢包的資金變化額度,適用於以太幣的金額變化,或者ERC-20代幣的金額變化。

單個錢包地址的金額變化:

await expect(() =&gt;
  sender.sendTransaction({ to: someAddress, value: 200 })
).to.changeEtherBalance(sender, "-200");
await expect(token.transfer(account, 1)).to.changeTokenBalance(
  token,
  account,
  1
);

也可以用來檢測多個賬戶的金額變化,在測試轉賬交易時,非常適用:

await expect(() =>
  sender.sendTransaction({ to: receiver, value: 200 })
).to.changeEtherBalances([sender, receiver], [-200, 200]);
await expect(token.transferFrom(sender, receiver, 1)).to.changeTokenBalances(
  token,
  [sender, receiver],
  [-1, 1]
);
  • 字串比較

可以用hardhat-chai-matchers提供的方法,方便地校驗各種複雜的字串,比如一個字串是否是正確的地址格式、私鑰格式等,用法如下:

// 是否符合address格式
expect("0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2").to.be.a.properAddress;
//是否符合私鑰格式
expect(SOME_PRI_KEY).to.be.a.properPrivateKey;
//判斷十六進位制字串的用法
expect("0x00012AB").to.hexEqual("0x12ab");
//判斷十六進位制字串的長度
expect("0x123456").to.be.properHex(6);

以上就是Hardhat進行合約測試環境準備及方法詳解的詳細內容,更多關於Hardhat合約測試的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com