テストケースを追加する

HardhatでNFTのテストコードを作成しようHardhatでNFTのテストコードを作成しよう

前回はmintが正常にできるかのテストを書きました。

今回はテストケースを追加しながら、よく使う便利機能についても確認します。

追加するテストの概要と雛形作成

今回は「Ownerは0ETHでmintできること」を確認してみましょう。

まずは、下記のようにitを追加して、テストの箱を追加します。

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

describe("mint関連機能のテスト", function () {
  it("mint関数を叩いたら、ウォレットにNFTが紐つけられること", async function () {
    const nft = await ethers.getContractFactory("NFT");

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

    const hardhatToken = await nft.deploy(
      'NFT',
      'NF',
      'ipfs//metadataのCID/',
      'invalid'
    );
    // ...
  });

  // ここを追加
  it("Ownerは0ETHでmintできること", async function () {
    // まだ空で良い
  });
});

このNFTのコントラクトは、Ownerであればフリーmintできるような仕様になっています。

実装箇所は下記↓。

  function mint(uint256 _mintAmount) public payable {
    ...
    // Ownerであれば、ETHのチェックは行われない
    if (msg.sender != owner()) {
      require(msg.value >= cost * _mintAmount, "eth is not enough!!");
    }

この機能がちゃんと動いているか、テストで確認していきます。

セットアップ処理を共通化する

が、実はここでちょっとした問題があります。

Hardhatをはじめ、多くのテストフレームワークでよくある事ですが、it()のテストをこなすごとに、ネットワークの状態が初期化されます。

すると、下記のような事が起こります。

describe("mint関連機能のテスト", function () {
  it("mint関数を叩いたら、ウォレットにNFTが紐つけられること", async function () {
    // ...
    // 1. NFTをデプロイし、addr1にNFTが一つ割り当てられるテスト
  });
  // 2. ↑のitが終わったら、ネットワークをリセットする
  //    つまりデプロイしたスマートコントラクトも無くなるし、addr1に割り当てられたNFTも消える

  it("Ownerは0ETHでmintできること", async function () {
    // 3. もう一度スマートコントラクトをデプロイし、ウォレットを取得し。。。みたいなセットアップ処理が必要になる
  });
});

つまり、初めのit()で行われた変更は、次のit()には適用されません。

よって、全てのit()の最初で

  • スマートコントラクトをデプロイし
  • ウォレットを取得

みたいなことをやる必要があります。

重複コードが多くなるし、パフォーマンスも落ちてしまいます。

テストケースごとに状態がリセットされるのは
他のテストフレームワークでもよくあることです。

他のテストケースが実行中のテストケースに影響を与えないという点で、
テストの影響範囲を小さくでき、あるべき姿と言えます。

そこで、fixtures機能を使います。

下記のようにコードを変更してください。

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

// 1. fixturesを使うための関数import
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

describe("mint関連機能のテスト", function () {

  // 2. セットアップ処理の内容を記載。デプロイやウォレット取得を行う
  async function deployNftFixture() {
    const nft = await ethers.getContractFactory("NFT");

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

    const hardhatToken = await nft.deploy(
      'NFT',
      'NF',
      'ipfs//metadataのCID/',
      'invalid'
    );
    await hardhatToken.deployed();

    // 3. itから呼ばれた際に、返却する変数たちを定義
    return { nft, hardhatToken, owner, addr1, addr2 };
  }

  it("mint関数を叩いたら、ウォレットにNFTが紐つけられること", async function () {
    // 4. loadFixtureを通して、セットアップ処理をたたき、各種変数を取得
    const { hardhatToken, addr1 } = await loadFixture(deployNftFixture);

    await hardhatToken.connect(addr1).mint(1, { value: ethers.utils.parseEther("0.0005") });

    const tokenIds = await hardhatToken.walletOfOwner(addr1.address);

    expect(tokenIds).to.deep.equal([ ethers.BigNumber.from("1") ]);
  });

  it("Ownerは0ETHでmintできること", async function () {
    // 5. loadFixtureを通して、セットアップ済みのOwnerウォレットを取得しておく
    const { hardhatToken, owner } = await loadFixture(deployNftFixture);
  });
});

流れとしては

  • deployNftFixture関数を定義し、デプロイやウォレットの取得等を行う
  • loadFixtureを使い、deployNftFixtureを呼び出し、デプロイ実行&必要なウォレット取得を行う

です。

これでitごとに同じコードを書く必要はなくなるし、fixturesの機能のおかげで処理も速くなります。

fixturesについての解説はこちらにあるので、気になる人は覗いてみてください。

追加のテストを書く

これで準備が整いました。

「Ownerは0ETHでmintできること」のテストを書いてみましょう。

describe("mint関連機能のテスト", function () {

  ...

  it("Ownerは0ETHでmintできること", async function () {
    // 1. loadFixtureを通して、セットアップ済みのOwnerウォレットを取得しておく
    const { hardhatToken, owner } = await loadFixture(deployNftFixture);

    // 2. ownerのウォレットで接続して、0ETHでmint関数を叩く
    await hardhatToken.connect(owner).mint(1, { value: ethers.utils.parseEther("0") });

    // 3. ownerアドレスが持つNFTのIDを取得
    const tokenIds = await hardhatToken.walletOfOwner(owner.address);

    // 4. ID1がownerのウォレットに紐ついていること
    expect(tokenIds).to.deep.equal([ ethers.BigNumber.from("1") ]);
  });
});

と言っても、テストの内容は前回とほぼ同じです。

変更点は下記。

  • 接続するウォレットをownerにする
  • mint時のETHの数値を0に変える

では、テストを実行してみます。

$ npx hardhat test


  mint関連機能のテスト
    ✔ mint関数を叩いたら、ウォレットにNFTが紐つけられること (2673ms)
    ✔ Ownerは0ETHでmintできること (78ms)


  2 passing (3s)

追加のテストを書く(エラーの場合)

次に、エラーのテストケースも書いてみましょう。

具体的には、「cost未満のETHでmint関数が叩かれた時エラーとなること」を確認してみます。

コントラクトの下記部分の実装ですね。

  uint256 public cost = 0.0005 ether;
..
  function mint(uint256 _mintAmount) public payable {
    ...
    if (msg.sender != owner()) {
      // ここ
      require(msg.value >= cost * _mintAmount, "eth is not enough!!");
    }

requireは、第一引数の条件が偽であれば第二引数のメッセージと共にrevertが行われます。

revertとは状態を元に戻し、処理を中断することです。

要するに0.0005ETHのcostに対してETHが足りないよ。mintさせずにエラーにしますよ。って実装です。

このようなrevertについても、テストコードでテストできます。

下記のような感じです。

describe("mint関連機能のテスト", function () {
  ...
  it("cost未満のETHでmint関数が叩かれた時エラーとなること", async function () {
    const { hardhatToken, addr1 } = await loadFixture(deployNftFixture);
    // わざとETHを0.0005ETH未満にして、1つmintしようとする
    await expect(hardhatToken.connect(addr1).mint(1, { value: ethers.utils.parseEther("0.00049") })).to.be.revertedWith('eth is not enough!!')
  });
});

await expect(エラーとなる処理).to.be.revertedWith(revertメッセージ)

で、revertされるかどうかをテストできます。

ではテスト実行してみます。

$ npx hardhat test --grep "cost未満のETHでmint関数が叩かれた時エラーとなること"
Compiled 1 Solidity file successfully


  mint関連機能のテスト
    ✔ cost未満のETHでmint関数が叩かれた時エラーとなること (2904ms)


  1 passing (3s)

万が一mint関数にバグが混入しrevertがされなかった場合は、下記のようにテストエラーとなります。

$ npx hardhat test --grep "cost未満のETHでmint関数が叩かれた時エラーとなること"


  mint関連機能のテスト
    1) cost未満のETHでmint関数が叩かれた時エラーとなること


  0 passing (3s)
  1 failing

  1) mint関連機能のテスト
       cost未満のETHでmint関数が叩かれた時エラーとなること:
     AssertionError: Expected transaction to be reverted with reason 'eth is not enough!!', but it didn't revert

特定の条件下でエラーになることのテストは非常に重要です。

やり方をマスターしておいてください。

さらに他のテストを書いてみる

これで基本のテスト方法を学ぶことができました。

ただ、まだまだこのコントラクトのテストは不足しています。

例えば

  • 1度にmintできる個数をオーバーしたらmintできないこと
  • mint停止状態にしたら、mintできないこと

等々、セール日までにテストすべきテストケースが山積みです。

余裕のある方は、↑のテストも書いてみると良いです。

次のセクションへ

これで

  • セットアップ処理を共通化
  • セットアップ処理を使った上で、テストを追加

ができました。

次のセクションが最後です。

最後は、テストコードを使ってデバッグしながら開発する手法をご紹介します。

コメント

タイトルとURLをコピーしました