NFTなんもわからんのでとりあえず試してみた②〜Hardhatでテスト実装編〜

こんにちは、エンジニアの黒岩(@kro96_xr)です。 バックエンドを中心にフロントエンドやらインフラやら色々担当しています。

前回自分の記事ではブラウザ上で動作するRemix IDEを使ってコントラクトの実装を試してみました。

synamon.hatenablog.com

今回はその続編として、Hardhatを使ったコントラクトのテスト周りについて書いていきたいと思います。

Hardhatとは

Hardhatとは公式サイトにあるOverviewの言葉を借りると「Ethereumソフトウェアのコンパイル、デプロイ、テスト、およびデバッグを行うための開発環境」とのことです。Ethereumソフトウェア=Solidityで実装されたコントラクトという感じでしょうか。

また、「Hardhatには、開発用に設計されたローカルなEthereumネットワークであるHardhat Networkが内蔵されている」ため、ローカルでのテストを行うことができます。

検証環境

  • OS
    • macOS Big Sur 11.4 (Apple M1)
  • Node.js
    • v16.13.2
  • npm
    • 8.1.2

インストール

新しいディレクトリを作りインストールします。詳しくは公式をご覧ください。

$ mkdir hardhat
$ cd hardhat
$ npm init -y
$ npm install --save-dev hardhat

ついでにOpenZeppelinもインストールしておきます。

$ npm i @openzeppelin/contracts

プロジェクト作成

npx hardhatを実行するとプロジェクトを作成できます。

$ npx hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.9.9 👷‍

作成時にプロジェクトの目的などを聞かれますので選択してEnterで進んでいきます。今回は全てデフォルトのままです。

? What do you want to do? … 
❯ Create a basic sample project
  Create an advanced sample project
  Create an advanced sample project that uses TypeScript
  Create an empty hardhat.config.js
  Quit

? Hardhat project root: › /path/to/project/hardhat
? Do you want to add a .gitignore? (Y/n) › y
? Do you want to install this sample project's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? (Y/n) › y

これでプロジェクトに必要なファイルが自動的に生成されます。

コントラクトの実装

コントラクトの実装はcontracts/以下に行います。
今回は前回実装したコードを少し修正し、mint時に既存のトークン数や1回にmintできるトークン数をチェックするロジックを入れてみました。

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract KrocksNFT is ERC721, ERC721Enumerable, Ownable {
    // 定数
    uint256 public constant MAX_SUPPLY = 10;
    uint256 public constant MAX_MINT_PER_TRANSACTION = 5;

    // コンストラクタ
    constructor() ERC721("Krocks NFT", "KRONFT") {}

    // mint時のロジック
    function mint(uint256 numberOfTokens) public payable {
        uint256 ts = totalSupply();
        require(
            numberOfTokens <= MAX_MINT_PER_TRANSACTION,
            "Exceeded max token per transaction"
        );
        require(
            ts + numberOfTokens <= MAX_SUPPLY,
            "Exceed max tokens"
        );

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _safeMint(msg.sender, ts + i);
        }
    }

    // BeforeTransfer
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }
    
    // SupportInterface
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

テストコードの実装

テストの実装はtests/以下にJavaScriptで実装します。
先程実装したバリデーションに関してテストしてみます。

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

describe("KrocksNFT", function () {
  let KrocksNFT, krocksNFTcontruct, addr1
  beforeEach(async function () {
    ;[owner, addr1] = await ethers.getSigners()
    // デプロイ
    KrocksNFT = await ethers.getContractFactory("KrocksNFT");
    krocksNFTcontruct = await KrocksNFT.deploy();
    await krocksNFTcontruct.deployed();
  })

  describe("mint", function () {
    it('Should be reverted if exceeded max token purchase', async function () {
      await expect(
        // 1回で6個のトークンをmint
        krocksNFTcontruct.connect(addr1).mint(6),
      ).to.be.revertedWith('Exceeded max token per transaction')
    })

    it('Should be reverted because the caller exceeds max token', async function () {
      //10個のトークンをmint
      for (let i = 0; i < 2; i++) {
        await krocksNFTcontruct.connect(addr1).mint(5)
      }
      // 11個目のトークンをmint
      await expect(
        krocksNFTcontruct.connect(addr1).mint(1),
      ).to.be.revertedWith('Exceed max total tokens')
    })
  })
})

テストを実行

$ npx hardhat test

  KrocksNFT
    mint
      ✔ Should be reverted if exceeded max token purchase
      ✔ Should be reverted because the caller exceeds max token (71ms)


  2 passing (619ms)

というわけで無事にテストが通りました。 コントラクトはデプロイ後の修正が出来ないのでしっかりテストしておきたいですね。

おわりに

以上、今回はHardhatを使ったコントラクトのテストについて書いてみました。
今回はローカルネットワークへのデプロイまでは行っていないので今後はその辺りについて書ければいいなと思っています。

参考

以下のサイトを参考にさせていただきました、ありがとうございます!

Hardhat | Ethereum development environment for professionals by Nomic Foundation
公式です。

GitHub - a3994288/erc-721-hardhat-test
やりたいことがドンピシャで実装されており、かなり簡易化して参考にさせてもらいました。
ちゃんと理解できるようにこれからも参考にさせていただきます。

Hardhatで始めるスマートコントラクト開発 | DevelopersIO
ちょうど書こうと思っていた内容が1週間前に公開されていました。いつもお世話になっております。