テストを使ってデバッグする

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

前回はfixturesを使ったセットアップ方法やテストケースを追加する方法を学びました。

今回はテストコードを使ってデバッグする方法を紹介します。

コントラクトのデバッグログを出す

現在のテストコードは下記でした。

const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

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

  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();

    return { nft, hardhatToken, owner, addr1, addr2 };
  }

  it("mint関数を叩いたら、ウォレットにNFTが紐つけられること", async function () {
    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 () {
    const { hardhatToken, owner } = await loadFixture(deployNftFixture);

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

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

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

ここで万が一。

「mint関数を叩いたら、ウォレットにNFTが紐つけられること」のテストがエラーになったとしましょう。

その際に、スマートコントラクト側にログを入れて解析することが可能です。

では、mint関数にログを入れてみましょう。

下記の★部分のように記載します。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "hardhat/console.sol";  // ★追加

...

  // public
  function mint(uint256 _mintAmount) public payable {
    uint256 supply = totalSupply();
    require(!paused);
    require(_mintAmount > 0);
    require(_mintAmount <= maxMintAmount);
    require(supply + _mintAmount <= maxSupply);

    // ★追加
    console.log(
      " cost confirmation.\n value: %s,\n sender: %s,\n owner: %s",
      msg.value,
      msg.sender,
      owner()
    );

hardhat/console.solをimportし、console.logを使うことで、コントラクト中の値を自由に出力することができます。

この状態でテストを実行し、mint関数を叩いてみましょう。

$ npx hardhat test --grep "mint関数を叩いたら、ウォレットにNFTが紐つけられること"
Compiled 1 Solidity file successfully


  mint関連機能のテスト
 cost confirmation.
 value: 500000000000000,
 sender: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8,
 owner: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    ✔ mint関数を叩いたら、ウォレットにNFTが紐つけられること (1629ms)


  1 passing (2s)

こんな感じで、テストをしたターミナル上でコントラクト中の値を確認できるようになります。

ゆるいテスト駆動開発のすすめ

Web2の開発手法の一つに、テスト駆動開発というものがあります。

プロダクションコードより先に、テストを書いて、都度テストを動かしながら開発していく手法です。

厳密なテスト駆動開発は慣れが必要なため、まずはゆるいテスト駆動開発をすると捗るかなと思います。

ゆるいテスト駆動開発の例

例えば、

  • 自分が所有しているNFTをバーンして
  • 新しいNFTをゲットするburnin関数

をコントラクトに書こうとしたとしましょう。

その場合、テスト駆動開発の決まりにゆるく則ると、

まずはテストコード上でburnin関数を呼び出す処理を書いてしまいます。

  it("burnin関数のテスト", async function () {
    const { hardhatToken, addr1 } = await loadFixture(deployNftFixture);

    await hardhatToken.connect(addr1).burnin();

    // expectはまだ書かない。テスト駆動開発原理主義はこの時点でexpectも書く
  });

この状態でテストを実行すると、burnin関数なんてないよ! とエラーが出ます。

$ npx hardhat test --grep "burnin関数のテスト"


  mint関連機能のテスト
    1) burnin関数のテスト


  0 passing (3s)
  1 failing

  1) mint関連機能のテスト
       burnin関数のテスト:
     TypeError: hardhatToken.connect(...).burnin is not a function
      at Context.<anonymous> (test/NFT.js:46:39)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)

よしよし、それじゃあコントラクトに何もしないburninっていう関数をとりあえず書いちゃうか。

...
  function burnin()
    public
    view
  {
    console.log('burnin!!!!!!!!');
  }
...

よし書いた!

試しにテストを通してみると。。。

$ npx hardhat test --grep "burnin関数のテスト"
Compiled 1 Solidity file successfully


  mint関連機能のテスト
burnin!!!!!!!!
    ✔ burnin関数のテスト (2337ms)


  1 passing (2s)

OK!

じゃあお次はバーンするNFTのIDを引数で指定できるようにしてみるか。。。

...
  function burnin(uint256 _burnTokenId)
    public
    view
  {
    console.log('burnin!!!!!!!!');
    console.log(_burnTokenId);
  }
...

テストで適当なIDを指定してburnin関数を呼び出して

  it("burnin関数のテスト", async function () {
    const { hardhatToken, addr1 } = await loadFixture(deployNftFixture);

    await hardhatToken.connect(addr1).burnin(1);

  });

テスト実行!!

$ npx hardhat test --grep "burnin関数のテスト"
Compiled 1 Solidity file successfully


  mint関連機能のテスト
burnin!!!!!!!!
1
    ✔ burnin関数のテスト (2659ms)


  1 passing (3s)

よし!! 引数が渡せたぞ!!

みたいなイメージです。

これを繰り返すことで、実ネットワークで作るよりはるかに速く開発を行うことができます。

ぜひ、ゆるいテスト駆動開発をやってみてくださいね。

テストコードまとめ

最後にテストコードのメリットについてまとめておきます。

仕様を担保しデグレードを防ぐ

テストコードの最大のメリットは、テストをコードで書いておくことでいつでも全テストを気軽に試せる点です。

例えば、mint関数が完成した後、ひょんなことからmint関数を修正した結果 、

既存の仕様とは異なる動きをするようになってしまった場合。

テストコードで仕様が書かれていれば、バグを拾ってくれます。

スマートコントラクトは一度デプロイしたら基本的に挙動を変えるのは困難です。

もし重大なバグを埋め込んだままセール日を迎えた場合。

大損害を被る可能性があります。

その可能性を低める意味でも、テストコードを書いておいて損はありません。

ゆるいテスト駆動開発は生産性を高める

生産性の観点でもテストコードの恩恵は計り知れません。

前章で紹介したように、テストコード起点でコントラクトを次々動かしていくと、開発の生産性がかなり高くなります。

想像してみてください。

もしテストコードがない場合。

一つの関数をデバッグするのに、コンパイルしてテストネットにデプロイして、Etherscanから関数叩いて、みたいなことをやる必要があります。(ローカルネットワークを使う人もいるかもしれません)

そういうまどろっこしい手作業がなくなり、ガンガン途中のコントラクトをデバッグできるのがテストコードの強みです。

また、fixturesなどを使いつつ、ウォレットにあらかじめNFTを入れておくみたいなことも可能になります。

ウォレットの状態によって挙動が変わるburninみたいなものは、実ネットワークでの検証が非常にめんどくさいです。

その点、テストコードであれば、ローカルのネットワークで、1コマンドで、何度もテストできるんです。

テストコードを書かない理由はありませんよね?

それではみなさん、良いテストライフをお過ごしください。

コメント

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