广告位联系
返回顶部
分享到

Hardhat进行合约测试环境准备及方法

JavaScript 来源:互联网 作者:佚名 发布时间:2023-03-09 21:57:36 人浏览
摘要

引言 Hardhat是一个开源的以太坊开发框架,简单好用、可扩展、可定制的特点让它在开发者中间很受欢迎。Hardhat在支持编辑、编译、调试和部署合约方面都非常的方便,也有很多功能可

引言

Hardhat是一个开源的以太坊开发框架,简单好用、可扩展、可定制的特点让它在开发者中间很受欢迎。Hardhat在支持编辑、编译、调试和部署合约方面都非常的方便,也有很多功能可以使合约测试工作更加高效和便捷,本文就是聚焦在合约测试领域,探寻Hardhat的特点和日常测试过程中的一些使用技巧。

一、环境准备

可以参考Hardhat官网教程,进行环境的准备和Hardhat安装。

Hardhat提供了快速构建合约工程的方法:

  • 建立空的工程目录
  • 在目录下执行npx hardhat
  • 根据交互提示完成Hardhat工程的创建

二、示例合约与测试方法

快速创建Hardhat工程,可以在contract目录下看到Lock.sol的合约,此合约是一个简单的示例,实现了在指定时间前(unlockTime)锁定资产的功能。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

// 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函数被调用后出现的回滚情况:

1

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的用法

官方文档示例如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

```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用法

1

2

3

4

5

6

   contract Ademo {

    event Event();

    function callFunction () public {

        emit Event();

    }

}

对合约C的call方法进行调用会触发一个无参数事件,为了测试这个事件是否被触发,可以直接用hardhat-chai-matchers中的Events断言,用法如下:

1

2

3

4

const A=await ethers.getContractFactory("Ademo");

const a=await A.deploy();

//采用hardhat-chai-matchers的断言方式,判断Events是否触发

await expect(a.callFunction()).to.emit(a,"Event");

  • Reverts用法:

1

2

3

4

5

6

7

8

    //最简单的判断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的判定:

1

2

3

4

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)之间的数(包括边界值),超出范围后将会出现错误,比如:

1

2

3

4

5

6

7

8

9

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代币的金额变化。

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

1

2

3

4

5

6

7

8

await expect(() =&gt;

  sender.sendTransaction({ to: someAddress, value: 200 })

).to.changeEtherBalance(sender, "-200");

await expect(token.transfer(account, 1)).to.changeTokenBalance(

  token,

  account,

  1

);

也可以用来检测多个账户的金额变化,在测试转账交易时,非常适用:

1

2

3

4

5

6

7

8

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提供的方法,方便地校验各种复杂的字符串,比如一个字符串是否是正确的地址格式、私钥格式等,用法如下:

1

2

3

4

5

6

7

8

// 是否符合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);


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。

您可能感兴趣的文章 :

原文链接 : https://juejin.cn/post/7207730270014734391
    Tag :
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计