当前位置:   article > 正文

Solidity智能合约单元测试介绍_solidity-coverage

solidity-coverage

Solidity智能合约单元测试介绍

当前在各种区块链中,生态最全的要属兼容EVM的区块链,在该类区块链上的智能合约绝大部分使用Solidity编写。因此,对Solidity编写的智能合约进行单元测试便成为一项经常性的工作。本文简要的介绍一下怎样使用hardhat进行Solidity智能合约单元测试。

一、什么是Hardhat

我们来看其官方文档的描述:

Hardhat is a development environment to compile, deploy, test, and debug your Ethereum software.

意思为 Hardhat是一个编译,部署,测试和调试以太坊程序的开发环境,在这里本文只涉及到其测试功能。

Hardhat之前,我们使用truffle做为开发、部署和测试环境。作为后来者,Hardhat的功能更强大,因此现在我们一般使用Hardhat来作为智能合约开发和测试工具。

官方文档介绍了两种测试方式:ethers.js + WaffleWeb3.js + Truffle。在这里我们使用ethers.js + Waffle模式。

二、测试内容

我们进行单元测试,经常性的测试内容有:

  • 状态检查,例如合约部署后检查初始状态是否正确,函数调用后检查状态是否改变。一般状态检查为读取view函数。
  • 事件触发。基本上,合约中的关键操作都应该触发事件进行相应追踪。在单元测试中了可以测试事件是否触发,抛出的参数是否正确。
  • 交易重置。在测试一些非预期条件时,交易应当重置并给出相应的原因。使用单元测试可以检测是否重置及错误原因是否相同。
  • 函数计算。例如要计算不同条件下某函数的返回值(例如奖励值),我们需要循环调用 某个函数并输入不同的参数,看是否结果相符。
  • 完全功能测试。例如我们合约中涉及到了区块高度或者 区块时间,比如质押一年后才能提取。此时我们一般需要加速区块时间或者区块高度来进行测试。幸运的是,hardhat提供了接口可以方便的进行此项测试。
  • 测试覆盖率。包含代码覆盖率,函数覆盖率和分支覆盖率。一般情况下,应该追求 100%完全覆盖。比如你写了一个modifier,但是忘记加到函数上去了,而单元测试也漏掉了,此时代码覆盖就会显示该代码未测试,这样可以发现一些简单的BUG。特殊情况下或者确定有代码不会执行的情况下,不追求100%覆盖率。

接下来我们来详细介绍每项内容的测试方法。

三、示例合约

我们按照官方介绍新建一个示例工程Greeting。在工作目录下运行下列命令:

mkdir Greeting
cd Greeting
npm install --save-dev hardhat
npx hardhat
  • 1
  • 2
  • 3
  • 4

此时选择第二项,创建一个高级示例项目(当然也可以选第3项使用typescrit),等待依赖库安装完毕。

运行code .使用vocode打开当前目录。

我们可以看到项目的contracts目录下已经生成了一个示例合约Greeter.sol,内容如下:

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Greeter {
    string private greeting;

    constructor(string memory _greeting) {
        console.log("Deploying a Greeter with greeting:", _greeting);
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
        greeting = _greeting;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

代码比较简单,需要注意的是它使用了一个hardhat/console.sol插件,该插件可以在hardhat netwrok环境中打印出相应的值,方便开发时调试。可以看到,它支持占位符模式。

进一步查看其文档,它实现了类似Node.jsconsole.log格式,其底层调用是util.format。这里我们看到它只使用了%s这一种占位符。

四、示例测试

打开项目根目录下的test目录,我们可以看到有一个sample-test.js的文件,其内容如下:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Greeter", function () {
  it("Should return the new greeting once it's changed", async function () {
    const Greeter = await ethers.getContractFactory("Greeter");
    const greeter = await Greeter.deploy("Hello, world!");
    await greeter.deployed();

    expect(await greeter.greet()).to.equal("Hello, world!");

    const setGreetingTx = await greeter.setGreeting("Hola, mundo!");

    // wait until the transaction is mined
    await setGreetingTx.wait();

    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这里的测试也比较简单,一般使用describe来代表测试某个项目或者功能,使用it来代表具体某项测试。注意,describeit是函数,在javascript中,一切都是函数。因此,我们可以在describe中再次嵌套describe,这样最外层的describe代表整个项目,内部的describe代表某项目功能。

在该测试文件中,先进行了合约的部署,然后验证合约的状态变量greeting是否为部署时提供的Hello, world!。然后运行setGreeting函数改变问候语为Hola, mundo!,并再次验证更改后的greeting

五、运行测试

我们运行npx hardhat test ./test/sample-test.js,结果如下:

Compiled 2 Solidity files successfully


  Greeter
Deploying a Greeter with greeting: Hello, world!
Changing greeting from 'Hello, world!' to 'Hola, mundo!'
    ✔ Should return the new greeting once it's changed (946ms)


  1 passing (949ms)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这里可以看到,我们打印出来了两个日志,刚好是我们合约中的console.log语句。

六、测试console

这里,console.log支持的数据类型有限,它仅支持4种数据类型:

  • uint
  • string
  • bool
  • address

但是它又提供了额外的API来支持其它类型,如console.logBytes(bytes memory b)等。详情见https://hardhat.org/hardhat-network/reference/#console-log

我们来简单测试一下,在Greeter.sol中添加如下函数:

function testConsole() public view returns(bool) {
    console.log("Caller is '%s'", msg.sender);
    console.log("Caller is '%d'", msg.sender);
    console.log("Caller is ", msg.sender);
    console.log("Number is '%s'", 0xff);
    console.log("Number is '%d'", 0xff);
    console.logBytes1(bytes1(0xff));
    console.logBytes(abi.encode(msg.sender));
    console.log("Reslut is ", true);
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

sample-test.js中添加一行代码expect(await greeter.testConsole()).to.be.equal(true);,再次运行npx hardhat test ./test/sample-test.js,结果如下:

Compiled 1 Solidity file successfully


  Greeter
Deploying a Greeter with greeting: Hello, world!
Changing greeting from 'Hello, world!' to 'Hola, mundo!'
Caller is '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'
Caller is '1.3908492957860717e+48'
Caller is  0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Number is '255'
Number is '255'
0xff
0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
Reslut is  true
    ✔ Should return the new greeting once it's changed (707ms)


  1 passing (709ms)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可以看到,当我们把地址类型当成整数打印时,它打印了对应的整数值。通常情况下,对于console.log支持的四种类型,我们可以不使用通配符或者全部使用%s作为字符串输出,特殊类型的数据使用相应的API进行打印。

七、事件测试

我们知道,合约中重要的操作基本上都会触发事件,因此,捕获抛出的事件并检查事件中的参数也是一项经常性的工作。在合约中添加如下代码。

function eventTest() public {
    emit CallerEmit(msg.sender, 500);
}
  • 1
  • 2
  • 3

我们这次修改我们的测试文件,将各功能均写一个describe来进行,代码如下:

const { expect, util, assert } = require("chai");
const { ethers } = require("hardhat");

describe("Greeter", function () {
  let greeter;
  let owner, user1, users;

  beforeEach(async () => {
    [owner, user1, ...users] = await ethers.getSigners();
    const Greeter = await ethers.getContractFactory("Greeter");
    greeter = await Greeter.deploy("Hello, world!");
    await greeter.deployed();
  });
  describe("State check test", function () {
    it("Should return the new greeting once it's changed", async function () {
      expect(await greeter.greet()).to.equal("Hello, world!");

      const setGreetingTx = await greeter.setGreeting("Hola, mundo!");

      // wait until the transaction is mined
      await setGreetingTx.wait();
      expect(await greeter.greet()).to.equal("Hola, mundo!");
    });
  });

  describe("Console test", function () {
    it("Console.log should be successful", async function () {
      expect(await greeter.testConsole()).to.be.equal(true);
    });
  });

  describe("Event test", function () {
    it("owner emit test", async () => {
      await expect(greeter.eventTest())
        .to.be.emit(greeter, "CallerEmit")
        .withArgs(owner.address, 500);
    });
    it("user1 emit test", async () => {
      await expect(greeter.connect(user1).eventTest())
        .to.be.emit(greeter, "CallerEmit")
        .withArgs(user1.address, 500);
    });
    it("Get emit params test", async () => {
      const tx = await greeter.connect(users[0]).eventTest();
      await tx.wait();
      const receipt = await ethers.provider.getTransactionReceipt(tx.hash);
      const hash = ethers.utils.solidityKeccak256(
        ["string"],
        ["CallerEmit(address,uint256)"]
      );
      const infos = receipt.logs[0];
      assert.equal(infos.topics[0], hash);
      const sender = ethers.utils.getAddress(
        "0x" + infos.topics[1].substring(26)
      );
      assert.equal(sender, users[0].address);
      const value = ethers.BigNumber.from(infos.data);
      expect(value).to.be.equal(500);
    });
  });
});
  • 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
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

可以看到,我们测试事件时进行了三项测试,分别为:

  1. 正常测试,主要是检查事件是否触发,参数是否正确。
  2. 同上,主要是切换合约调用者为user1
  3. 这里是解析事件来获取事件参数,此场景应用于某些事件参数无法提前获取等,比如一个伪随机数。

八、重置测试

我们来测试条件不满足时的交易重置,在合约中添加如下代码:

function revertTest(uint a, uint b) public {
    require(a > 10, "a <= 10");
    if(b > 10) {
        revert("b > 10 ");
    }else {
        revert();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

注意:这里会有编译警告,提示我们最后一个revert缺少提示字符串,我们是故意这样的,请忽略它。

在测试文件中添加如下describe

describe("Revert test", function () {
    it("a < 10 should be failed", async () => {
      await expect(greeter.revertTest(5, 5)).to.be.revertedWith("a <= 10");
    });
    it("b > 10 should be failed", async () => {
      await expect(greeter.revertTest(15, 55)).to.be.revertedWith("b > 10");
    });
    it("b < 10 should be failed", async () => {
      await expect(greeter.revertTest(15, 5)).to.be.reverted;
    });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然后我们运行测试通过。

九、区块测试

当我们合约中的内容涉及到区块时,一般需要进行相应区块高度或者区块时间的条件测试。先在测试合约中添加如下内容:

function blockNumberTest() public {
    require(block.number >= 10000,"not matched block number");
    console.log("block number: %d", block.number);
}

function blockTimeTest() public {
    require(block.timestamp >= 1750631915,"not matched block time");
    console.log("block timestamp: %d", block.timestamp);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

编译时会提示上面两个函数为view函数,但是如果我们把它标记为view函数,那么测试时便不会mine一个新区块。为了模拟真实场景,我们不把它标记为view函数,从而在调用时产生一个新的区块。

然后在测试文件中增加如下describe

describe("Block test", () => {
    let block;
    let timestamp;
    
    // 用来去除16进制的左边自动补零
    function convertNum(num) {
        let big = ethers.BigNumber.from("" + num)
        let str = big.toHexString()
        let index = 0
        for(let i=2;i<str.length;i++) {
            if(str[i] !== "0") {
                index = i;
                break;
            }
        }
        if(index === 0) {
            return str;
        }else {
            return str.substring(0,2) + str.substring(index)
        }
    }
    
    beforeEach(async () => {
      block = await ethers.provider.getBlockNumber();
      timestamp = (await ethers.provider.getBlock(block)).timestamp;
    });
    // 注意,这里hardhat network 默认是一秒一个区块
  	it("Call before timestamp 1651631915 should be failed", async () => {
      assert.ok(timestamp < 1651631915);
      await expect(greeter.blockTimeTest()).to.be.revertedWith(
        "not matched block time"
      );
    });

    it("Call at timestamp 1651631915 should be successfult", async () => {
      await ethers.provider.send("evm_mine", [1651631915 - 1]);
      await greeter.blockTimeTest();
    });
    it("Call before block 10000 should  be failed", async () => {
      assert.ok(block < 10000);
      await expect(greeter.blockNumberTest()).to.be.revertedWith(
        "not matched block number"
      );
    });

    it("Call at block 10000 should be successful", async () => {
      let value = 10000 - block - 1;
      //快速推进到100000区块前一个
      await ethers.provider.send("hardhat_mine", [convertNum(value]);
      await greeter.blockNumberTest();
    });
});
  • 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
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

注意,在上面的子describe中又使用了beforeEach函数。这里讲一下beforeEachbefore的区别,beforeEach 顾名思义,在每项it测试前都会执行一次;而before,在一个describe中只会执行一次。

这里it函数要使用的describe函数内的变量都放在describe中定义,通常我们测试时会使用一个全新的状态,所以一般使用beforeEach而不是before。但特殊场景会有时会使用before,比如后面的测试依赖于前面的测试结果的。

执行测试后输出的结果显示,我们确定是在block == 10000timestamp == 5555555555调用了相应的函数。

这里,我们采用的是hardhat自动出块策略。此时,每笔交易不管成功还是失败,都会出一个块,并且每个区块内就只有一个交易。但是如果我们想一个区块内包含多个交易怎么办?hardhat也提供了相应的rpc接口,例如evm_setAutomineevm_setIntervalMining来模拟真实的出块场景。

我们在测试合约中增加如下代码:

uint public curBlock;
uint public counter;
modifier oneBlock() {
    if(curBlock != 0) {
        require(block.number == curBlock,"not in one block");
    }
    _;
    if(curBlock == 0) {
        curBlock = block.number;
    }

}
function addCounter() external oneBlock {
    counter ++;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里增加了一个函数addCounter用来在一个区块内改变记数器。如果不在一个区块内,则会revert。

相应的测试文件增加如下代码,在Block test里增加:

it("addCounter test", async () => {
    expect(await greeter.counter()).to.be.equal(0);
    expect(await greeter.curBlock()).to.be.equal(0);
    //模拟正常出块,3秒一个
    await network.provider.send("evm_setAutomine", [false]);
    await network.provider.send("evm_setIntervalMining", [3000]);
    let tx1 = await greeter.addCounter();
    let tx2 = await greeter.addCounter();
    let tx3 = await greeter.addCounter(); 
    const pendingBlock = await network.provider.send("eth_getBlockByNumber", [
      "pending",
      false,
    ]);
    assert.equal(pendingBlock.transactions.length,3);
    assert.equal(pendingBlock.transactions[0],tx1.hash);
    assert.equal(pendingBlock.transactions[1],tx2.hash);
    assert.equal(pendingBlock.transactions[2],tx3.hash);
    await tx1.wait()
    expect(await greeter.counter()).to.be.equal(3);
    expect(await greeter.curBlock()).to.be.equal(block + 1);
    await network.provider.send("evm_setAutomine", [true]);
    await expect(greeter.addCounter()).to.be.revertedWith("not in one block");
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

十、测试覆盖率

打开hardhat.config.js,我们可以看到有如下两行:

require("hardhat-gas-reporter");
require("solidity-coverage");
  • 1
  • 2

分别是gas消耗统计插件和测试覆盖率插件。这里我们只介绍第二个插件的用法。由于插件已经装好了,我们直接运行如下命令:

npx hardhat coverage
  • 1

预料之外的事情发生了,提示我们gasPrice太小了,这个笔者也不知道原因,不过我们可以解决它。

将上面的相关代码替换为如下:

await greeter.blockTimeTest({
  gasPrice: ethers.utils.parseUnits("10", "wei"),
});

await greeter.blockNumberTest({
  gasPrice: ethers.utils.parseUnits("20", "wei"),
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

好了,我们再次运行npx hardhat coverage。四个100%,Perfect !

然而我们这里有点小问题,如果更换blockNumberblockTime的测试顺序,这里仍然会有错误提示,这里估计是插件库之间不兼容造成的吧,暂无解决方案。

我们添加一个未完全测试的函数:

function getValue(uint a) public pure returns(uint) {
    if(a > 10) {
        return 3;
    }else {
        return 1;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们新添加一个describe,这里同样会出现block测试时的错误,因此我们将它放到Block test之前,Revert test 之后。

再次运npx hardhat coverage,得到如下结果:

  13 passing (1s)

--------------|----------|----------|----------|----------|----------------|
File          |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
--------------|----------|----------|----------|----------|----------------|
 contracts/   |    96.15 |       90 |      100 |    96.15 |                |
  Greeter.sol |    96.15 |       90 |      100 |    96.15 |             66 |
--------------|----------|----------|----------|----------|----------------|
All files     |    96.15 |       90 |      100 |    96.15 |                |
--------------|----------|----------|----------|----------|----------------|
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到66行代码没有测试到。这里只有一行未测试到,所以很直接。当我们有很多行未测试到时,怎么查看呢?这小小的地方是显示不了那么多行数的。

Coverage同时提供了一个网页版的结果图,在项目根目录下找到coverage目录并点开,右键点击index.html并选择在默认浏览器中打开。类似如下图:
在这里插入图片描述

点击那个contracts目录,会显示下面所有的合约,这里就只有一个,Greeter.sol。然后再点击Greeter.sol,会进入源代码界面,如下图:
在这里插入图片描述

这里的1x,3x代表执行次数,E应该是代表分支情况吧,粉色背景的就是未执行的代码了。(这里我也是猜的,但是1x代表执行次数是确定的。

我们只需要在测试文件里再调用 一次getValue并将参数设置为5,就又可以达到 100%覆盖了。

好了,基本的单元测试方法介绍就结束了,更深层次的了解还是需要多看hardhat自己的官方文档。

十一、最终代码

最终合约文件Greeter.sol为:

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";


contract Greeter {
    string private greeting;

    event CallerEmit(address indexed sender, uint value);

    constructor(string memory _greeting) {
        console.log("Deploying a Greeter with greeting:", _greeting);
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
        greeting = _greeting;
    }

    function testConsole() public view returns(bool) {
        console.log("Caller is '%s'", msg.sender);
        console.log("Caller is '%d'", msg.sender);
        console.log("Caller is ", msg.sender);
        console.log("Number is '%s'", 0xff);
        console.log("Number is '%d'", 0xff);
        console.logBytes1(bytes1(0xff));
        console.logBytes(abi.encode(msg.sender));
        console.log("Reslut is ", true);
        return true;
    }

    function eventTest() public {
        emit CallerEmit(msg.sender, 500);
    }

    function revertTest(uint a, uint b) public {
        require(a > 10, "a <= 10");
        if(b > 10) {
            revert("b > 10 ");
        }else {
            revert();
        }
    }

    function blockNumberTest() public {
        require(block.number >= 10000,"not matched block number");
        console.log("block number: %d", block.number);
    }

    function blockTimeTest() public {
        require(block.timestamp >= 1651631915,"not matched block time");
        console.log("block timestamp: %d", block.timestamp);
    }

    function getValue(uint a) public pure returns(uint) {
        if(a > 10) {
            return 3;
        }else {
            return 1;
        }
    }
}
  • 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
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

最终测试文件sample-test.js为:

const { expect, assert } = require("chai");
const { ethers } = require("hardhat");

describe("Greeter", function () {
  let greeter;
  let owner, user1, users;

  beforeEach(async () => {
    [owner, user1, ...users] = await ethers.getSigners();
    const Greeter = await ethers.getContractFactory("Greeter");
    greeter = await Greeter.deploy("Hello, world!");
    await greeter.deployed();
  });
  describe("State check test", function () {
    it("Should return the new greeting once it's changed", async function () {
      expect(await greeter.greet()).to.equal("Hello, world!");

      const setGreetingTx = await greeter.setGreeting("Hola, mundo!");

      // wait until the transaction is mined
      await setGreetingTx.wait();
      expect(await greeter.greet()).to.equal("Hola, mundo!");
    });
  });

  describe("Console test", function () {
    it("Console.log should be successful", async function () {
      expect(await greeter.testConsole()).to.be.equal(true);
    });
  });

  describe("Event test", function () {
    it("owner emit test", async () => {
      await expect(greeter.eventTest())
        .to.be.emit(greeter, "CallerEmit")
        .withArgs(owner.address, 500);
    });
    it("user1 emit test", async () => {
      await expect(greeter.connect(user1).eventTest())
        .to.be.emit(greeter, "CallerEmit")
        .withArgs(user1.address, 500);
    });
    it("Get emit params test", async () => {
      const tx = await greeter.connect(users[0]).eventTest();
      await tx.wait();
      const receipt = await ethers.provider.getTransactionReceipt(tx.hash);
      const hash = ethers.utils.solidityKeccak256(
        ["string"],
        ["CallerEmit(address,uint256)"]
      );
      const infos = receipt.logs[0];
      assert.equal(infos.topics[0], hash);
      const sender = ethers.utils.getAddress(
        "0x" + infos.topics[1].substring(26)
      );
      assert.equal(sender, users[0].address);
      const value = ethers.BigNumber.from(infos.data);
      expect(value).to.be.equal(500);
    });
  });

  describe("Revert test", function () {
    it("a < 10 should be failed", async () => {
      await expect(greeter.revertTest(5, 5)).to.be.revertedWith("a <= 10");
    });
    it("b > 10 should be failed", async () => {
      await expect(greeter.revertTest(15, 55)).to.be.revertedWith("b > 10");
    });
    it("b < 10 should be failed", async () => {
      await expect(greeter.revertTest(15, 5)).to.be.reverted;
    });
  });

  describe("Get value test", () => {
    it("a > 10 test", async () => {
      expect(await greeter.getValue(15)).to.be.equal(3);
    });
  });

  describe("Block test", () => {
    let block;
    let timestamp;
    
    // 用来去除16进制的左边自动补零
    function convertNum(num) {
        let big = ethers.BigNumber.from("" + num)
        let str = big.toHexString()
        let index = 0
        for(let i=2;i<str.length;i++) {
            if(str[i] !== "0") {
                index = i;
                break;
            }
        }
        if(index === 0) {
            return str;
        }else {
            return str.substring(0,2) + str.substring(index)
        }
    }
    
    beforeEach(async () => {
      block = await ethers.provider.getBlockNumber();
      timestamp = (await ethers.provider.getBlock()).timestamp;
    });
    
    it("addCounter test", async () => {
      expect(await greeter.counter()).to.be.equal(0);
      expect(await greeter.curBlock()).to.be.equal(0);
      //模拟正常出块,3秒一个
      await network.provider.send("evm_setAutomine", [false]);
      await network.provider.send("evm_setIntervalMining", [3000]);
      let tx1 = await greeter.addCounter();
      let tx2 = await greeter.addCounter();
      let tx3 = await greeter.addCounter(); 
      const pendingBlock = await network.provider.send("eth_getBlockByNumber", [
        "pending",
        false,
      ]);
      assert.equal(pendingBlock.transactions.length,3);
      assert.equal(pendingBlock.transactions[0],tx1.hash);
      assert.equal(pendingBlock.transactions[1],tx2.hash);
      assert.equal(pendingBlock.transactions[2],tx3.hash);
      await tx1.wait()
      expect(await greeter.counter()).to.be.equal(3);
      expect(await greeter.curBlock()).to.be.equal(block + 1);
      await network.provider.send("evm_setAutomine", [true]);
      await expect(greeter.addCounter()).to.be.revertedWith("not in one block");
    });
    
    // 注意,这里hardhat network 默认是一秒一个区块
    it("Call before timestamp 1651631915 should be failed", async () => {
      assert.ok(timestamp < 1651631915);
      await expect(greeter.blockTimeTest()).to.be.revertedWith(
        "not matched block time"
      );
    });

    it("Call at timestamp 1651631915 should be successfult", async () => {
      await ethers.provider.send("evm_mine", [1651631915 - 1]);
      await greeter.blockTimeTest({
        //如果不用coverage ,直接运行npx hardhat test ,下面的"wei"要改成"gwei"
        gasPrice: ethers.utils.parseUnits("10", "wei"),
      });
    });
    it("Call before block 10000 should  be failed", async () => {
      assert.ok(block < 10000);
      await expect(greeter.blockNumberTest()).to.be.revertedWith(
        "not matched block number"
      );
    });

    it("Call at block 10000 should be successful", async () => {
      let value = 10000 - block - 1;
      await ethers.provider.send("hardhat_mine", [convertNum(value]);
      await greeter.blockNumberTest({
        //如果不用coverage ,直接运行npx hardhat test ,下面的"wei"要改成"gwei"
        gasPrice: ethers.utils.parseUnits("20", "wei"),
      });
    });
  });
});
  • 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
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/826730
推荐阅读
相关标签
  

闽ICP备14008679号