Hardhat入门 | 测试你编写的智能合约

1,419 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

之前我们介绍了使用 Hardhat 进行合约编写,现在我们对之前写好的合约进行自动化测试。

测试合约

由于智能合约事关用户资金,因此为智能合约编写自动化测试脚本非常重要。为此,我们将使用 Hardhat Network 来测试我们之前编写好的智能合约。这是 Hardhat 专门为开发者设计的内置以太坊网络,无需进行任何设置即可使用它。在我们的测试中,我们将会使用 ethers.js 与前面构建的合约进行交互,并使用 Mocha 作为测试框架。

编写测试用例

在项目根目录中创建一个名为 test 的新目录,并创建一个名为 Token.js 的新文件,在文件中粘贴以下代码:

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

describe("Token contract",function() {
it("Deployment should assign the total supply of tokens to the owner",asyncfunction() {
const [owner] = await ethers.getSigners();

constToken = await ethers.getContractFactory("Token");

const hardhatToken = awaitToken.deploy();
await hardhatToken.deployed();

const ownerBalance = await hardhatToken.balanceOf(owner.getAddress());
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
  });
});

在终端上运行npx hardhat test。 你可以看到以下输出:

$ npx hardhat test
All contracts have already been compiled, skipping compilation.

  Token contract
    ✓ Deployment should assign the total supply of tokens to the owner (654ms)

  1 passing (663ms)

这意味着测试通过了。现在我们解释一下代码:

const [owner] =await ethers.getSigners();

ethers.js中的 Signer 代表以太坊账户对象。 它用于将交易发送到合约和其他帐户。在此,我们获得了所连接节点中的帐户列表,在本例中节点为 Hardhat Network,并且仅保留第一个帐户。

ethers 变量在全局作用域下都可用,相当于在顶部添加以下这一行:

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

✨要了解有关Signer的更多信息,可以查看Signers文档。

constToken = await ethers.getContractFactory("Token");

ethers.js中的 ContractFactory 是用于部署新智能合约的抽象,因此此处的 Token 是用来实例代币合约的工厂。

const hardhatToken = awaitToken.deploy();

ContractFactory 上调用 deploy() 将启动部署,并返回解析为 ContractPromise。 该对象包含了智能合约所有函数的方法。

await hardhatToken.deployed();

当你调用 deploy() 时,将发送交易,但是直到该交易打包出块后,合约才真正部署。 调用 deployed() 将返回一个 Promise,因此该代码将阻塞直到部署完成。

const ownerBalance =await hardhatToken.balanceOf(owner.getAddress());

部署合约后,我们可以在hardhatToken 上调用合约方法,通过调用balanceOf()来获取所有者帐户的余额。

部署合约的帐户获得了全部代币,在使用 hardhat-ethers 插件时,默认情况下, ContractFactoryContract 实例连接到第一个签名者。 这意味着 owner 变量中的帐户执行了部署,而 balanceOf() 应该返回全部发行量。

expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);

在这里,再次使用 Contract 实例调用Solidity代码中合约函数。 totalSupply() 返回代币的发行量,我们检查它是否等于 ownerBalance

判断相等,我们使用Chai,这是一个断言库。这些断言函数称为“匹配器”,在此实际上使用的“匹配器”来自Waffle

使用不同的账号

如果你需要从默认帐户以外的其他帐户(或 ethers.js 中的 Signer)发送交易来测试代码,则可以在ethers.js的 Contract 中使用 connect() 方法来将其连接到其他帐户,像这样:

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

describe("Transactions",function () {

it("Should transfer tokens between accounts",asyncfunction() {
const [owner, addr1, addr2] = await ethers.getSigners();

constToken = await ethers.getContractFactory("Token");

const hardhatToken = await Token.deploy();
await hardhatToken.deployed();

// Transfer 50 tokens from owner to addr1await hardhatToken.transfer(await addr1.getAddress(), 50);
expect(await hardhatToken.balanceOf(await addr1.getAddress())).to.equal(50);

// Transfer 50 tokens from addr1 to addr2await hardhatToken.connect(addr1).transfer(await addr2.getAddress(), 50);
expect(await hardhatToken.balanceOf(await addr2.getAddress())).to.equal(50);
  });
});

完整覆盖测试

我们已经介绍了测试合约所需的基础知识,以下是代币的完整测试用例,其中包含有关Mocha以及如何构组织测试的许多信息。 我们建议你通读。

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

describe("Token contract", function () {
  let Token;
  let hardhatToken;
  let owner;
  let addr1;
  let addr2;
  let addrs;

  beforeEach(async function () {
    // Get the ContractFactory and Signers here.
    Token = await ethers.getContractFactory("Token");
    [owner, addr1, addr2, ...addrs] = await ethers.getSigners();

    // To deploy our contract, we just have to call Token.deploy() and await
    // for it to be deployed(), which happens onces its transaction has been
    // mined.
    hardhatToken = await Token.deploy();
    await hardhatToken.deployed();

    // We can interact with the contract by calling `hardhatToken.method()`
    await hardhatToken.deployed();
  });

  // You can nest describe calls to create subsections.
  describe("Deployment", function () {
    // `it` is another Mocha function. This is the one you use to define your
    // tests. It receives the test name, and a callback function.

    // If the callback function is async, Mocha will `await` it.
    it("Should set the right owner", async function () {
      // Expect receives a value, and wraps it in an assertion objet. These
      // objects have a lot of utility methods to assert values.

      // This test expects the owner variable stored in the contract to be equal
      // to our Signer's owner.
      expect(await hardhatToken.owner()).to.equal(await owner.getAddress());
    });

    it("Should assign the total supply of tokens to the owner", async function () {
      const ownerBalance = await hardhatToken.balanceOf(owner.getAddress());
      expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
    });
  });

  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      // Transfer 50 tokens from owner to addr1
      await hardhatToken.transfer(await addr1.getAddress(), 50);
      const addr1Balance = await hardhatToken.balanceOf(
        await addr1.getAddress()
      );
      expect(addr1Balance).to.equal(50);

      // Transfer 50 tokens from addr1 to addr2
      // We use .connect(signer) to send a transaction from another account
      await hardhatToken.connect(addr1).transfer(await addr2.getAddress(), 50);
      const addr2Balance = await hardhatToken.balanceOf(
        await addr2.getAddress()
      );
      expect(addr2Balance).to.equal(50);
    });

    it("Should fail if sender doesn’t have enough tokens", async function () {
      const initialOwnerBalance = await hardhatToken.balanceOf(
        await owner.getAddress()
      );

      // Try to send 1 token from addr1 (0 tokens) to owner (1000 tokens).
      // `require` will evaluate false and revert the transaction.
      await expect(
        hardhatToken.connect(addr1).transfer(await owner.getAddress(), 1)
      ).to.be.revertedWith("Not enough tokens");

      // Owner balance shouldn't have changed.
      expect(await hardhatToken.balanceOf(await owner.getAddress())).to.equal(
        initialOwnerBalance
      );
    });

    it("Should update balances after transfers", async function () {
      const initialOwnerBalance = await hardhatToken.balanceOf(
        await owner.getAddress()
      );

      // Transfer 100 tokens from owner to addr1.
      await hardhatToken.transfer(await addr1.getAddress(), 100);

      // Transfer another 50 tokens from owner to addr2.
      await hardhatToken.transfer(await addr2.getAddress(), 50);

      // Check balances.
      const finalOwnerBalance = await hardhatToken.balanceOf(
        await owner.getAddress()
      );
      expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150);

      const addr1Balance = await hardhatToken.balanceOf(
        await addr1.getAddress()
      );
      expect(addr1Balance).to.equal(100);

      const addr2Balance = await hardhatToken.balanceOf(
        await addr2.getAddress()
      );
      expect(addr2Balance).to.equal(50);
    });
  });
});

这是 npx hardhat test在完整测试用例下输出的样子:

$ npx hardhat test
All contracts have already been compiled, skipping compilation.

  Token contract
    Deployment
      ✓ Should set the right owner
      ✓ Should assign the total supply of tokens to the owner
    Transactions
      ✓ Should transfer tokens between accounts (199ms)
      ✓ Should fail if sender doesn’t have enough tokens
      ✓ Should update balances after transfers (111ms)

  5 passing (1s)

✨当你运行npx hardhat test时,如果合约在上次运行测试后发生了修改,则会对其进行重新编译。

用 Hardhat Network 调试

Hardhat内置了 Hardhat Network ,这是一个专为开发而设计的以太坊网络。 它允许你部署合约,运行测试和调试代码。 这是Hardhat所连接的默认网络,因此你无需进行任何设置即可工作。 你只需运行测试就好。

Solidity 中使用 console.log

Hardhat Network 上运行合约和测试时,你可以在Solidity代码中调用console.log()打印日志信息和合约变量。 你必须先从合约代码中导入Hardhatconsole.log 再使用它。

例如这样:

pragma solidity ^0.8.16;

import "hardhat/console.sol";

contract Token {
//...
}

用法同JavaScript中差不多,将一些 console.log 添加到 transfer() 函数中:

function transfer(address to,uint256 amount)external {
    console.log("Sender balance is %s tokens", balances[msg.sender]);
    console.log("Trying to send %s tokens to %s", amount, to);

require(balances[msg.sender] >= amount, "Not enough tokens");

    balances[msg.sender] -= amount;
    balances[to] += amount;
}

运行测试时,将输出日志记录:

$ npx hardhat test
Compiling...
Compiled 2 contracts successfully

  Token contract
    Deployment
      ✓ Should set the right owner
      ✓ Should assign the total supply of tokens to the owner
    Transactions
Sender balance is 1000 tokens
Trying to send 50 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4
Sender balance is 50 tokens
Trying to send 50 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3
      ✓ Should transfer tokens between accounts (373ms)
      ✓ Should fail if sender doesn’t have enough tokens
Sender balance is 1000 tokens
Trying to send 100 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4
Sender balance is 900 tokens
Trying to send 100 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3
      ✓ Should update balances after transfers (187ms)

  5 passing (2s)

总结

至此,我们已经学会使用 HardHat 进行一些关于智能合约的开发了。当然,到目前为止,你所编写的合约还只是运行在你自己创建的测试链上,而想要部署到正式网络,你就需要花费一些“真金白银”并了解 Infura。最后是一些你开发时可能需要使用的文档: