Hardhat是一个开源的以太坊开发结构,简单好用、可扩展、可定制的特色让它在开发者中心很受欢迎。Hardhat在支撑修改、编译、调试和布置合约方面都十分的方便,也有许多功用能够使合约测验作业愈加高效和快捷,本文便是聚焦在合约测验范畴,探寻Hardhat的特色和日常测验过程中的一些运用技巧。

一、环境预备

能够参阅Hardhat官网教程,进行环境的预备和Hardhat安装。

Hardhat供给了快速构建合约工程的办法:

  1. 建立空的工程目录
  2. 在目录下履行npx hardhat
  3. 依据交互提示完成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的运用

  1. 运用场景

    用于每次履行测验前的setup操作,能够界说一个函数,在此函数中完成比方合约布置,合约初始化,账户初始化等操作,在每次履行测验前运用loadFixture的功用,进行相同的变量状况的设置,对合约测验供给了很大的帮助。

  2. 作业原理

    依据Hardhat源码,能够看到loadFixture维护了一个快照数组snapshots,一个快照元素包含:

    • fixture: Fixture类型的入参函数,type Fixture = () => Promise;
    • data:fixture函数中界说的状况变量
    • restorer:一个有restore办法的结构体,在“./helpers/takeSnapshot”办法中有界说,能够触发evm_revert操作,指定区块链退回到某个快照点。

    不同的函数f作为loadFixture入参时,会有不同的snapshot存储在loadFixture维护的snapshots数组中。

    在loadFixture(f)初次履行时,归于f函数的snapshot为undefined,此时会记载f函数中界说的全部状况变量,一起履行:

    const restorer = await takeSnapshot();

    并将此时的snapshot元素加入到snapshots数组中,后边再次用到同一个入参函数f的loadFixture时,在快照数组snapshots中已存在快照,可直接进行区块链状况回滚: await snapshot.restorer.restore();

  3. 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");
  1. 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();
  1. 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
  1. Balance Changes

能够很方便的检测用户钱包的资金改变额度,适用于以太币的金额改变,或许ERC-20代币的金额改变。

单个钱包地址的金额改变:

await expect(() =>
  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]
);
  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);