🐯

TypescriptでChaiとMochaを使ったSmart Contract単体テストの書き方

2022/12/20に公開

Ethereum Virtual Machine (EVM)にデプロイされたスマート コントラクトは、基本的に変更不可です。
リリース後にバグを修正することができることが当たり前になっているエンジニアにとってはなかなか慣れないものだと思います。

実際にはプロキシ パターンなどを使えばテスト可能ですが実装が複雑になります。
そのためSmart Contractのバグをリリース前に発見できるようにテストはとても大事になります

テストの種類には自動テストと手動テストの2種類あります。今回は自動テストのなかでもSmart Contractの単体テストに注目して基本的な書き方を紹介します。

今回はTypescriptを使用します。
JavascriptでSmart Contractに使用されるテストライブラリは、Waffle, Chai & Mocha,Truffleをよく見かけます。
今回は、HardhatではChaiとMochaを使ったテストが推奨されていたのでこのドキュメントに従いChaiとMochaで書いてみます。

事前準備

# hardhatと関連ライブラリのインストール
% npm install --save-dev hardhat 
% npm install @openzeppelin/contracts

hardhatをインストールするとchaiとmochaもインストールされるので別でインストールする必要はありません。

次に今回のサンプルコントラクトを作成します。
すでにコントラクトがある場合はスキップしてください。

% npx hardhat init

サンプルなので、Create a TypeScript projectを選択。他の選択肢もyesで進みます。
テンプレートが作られます。

テストを実行してみましょう。initで作成したサンプルプロジェクトにはすでにサンプルのテストが書かれています。
このコマンドで/test以下のテストが実行されます。

%npx hardhat test

テストを書いてみる

それでは実際のテストを書いてみましょう。
testディレクトリ以下に、sample.tsというファイルを作成します。(ファイル名は自由に決めてください)
今回はこのような簡単なmintができるスマートコントラクトを書いてみました。

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

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";


contract Sample is ERC1155, Ownable {

    string public name = "Sample";
    string public symbol = "Sample";

    constructor() ERC1155("https://hogehoge.com/metadata/{id}.json") {
    }

    function mint(uint256 amount) internal {
        _mint(msg.sender, 1, amount, "");
    }

    function setURI(string memory uri_) public onlyOwner {
        _setURI(uri_);
    }
}

以下はスマートコントラクトに対する簡単なテストです。
Hardhatのテストドキュメントのページを参考に書きました。

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

describe("Sample contract", function () {
  async function deployContractFixture() {
    // Sampleは使うコントラクト名に書き換えてください
    const Contract = await ethers.getContractFactory("Sample");
    const [owner, addr1, addr2] = await ethers.getSigners();

    const hardhatContract = await Contract.deploy();
    await hardhatContract.deployed();

    return { Contract, hardhatContract, owner, addr1, addr2 };
  }

  describe("Deployment", function () {
    it("Should deploy", async function () {
      const { hardhatContract, owner } = await loadFixture(
        deployContractFixture
      );
      expect(await hardhatContract.owner()).to.equal(owner.address);
    });
  });

  describe("Deployment", function () {
    it("Should set the right owner", async function () {
      const { hardhatContract, owner } = await loadFixture(
        deployContractFixture
      );
      expect(await hardhatContract.owner()).to.equal(owner.address);
    });
  });

  describe("mint", () => {
    it("user can mint", async () => {
      const tokenId = 1;
      const { hardhatContract, owner, addr1, addr2 } = await loadFixture(
        deployContractFixture
      );
      const mintTx = await hardhatContract.connect(addr1).mint(1);
      await mintTx.wait();
      const tokenCount = await hardhatContract.balanceOf(
        addr1.address,
        tokenId
      );
      expect(tokenCount).to.be.equal(1);
    });
  });
});

各テストが実行されるたびにデプロイされないように、deployContractFixtureというデプロイなどテストに必要な処理を行う関数を用意して各テストで呼び出すようにしています。
再度テストを実行してみましょう

%npx hardhat test

以下のような表示が出てテストが通っていることが確認できました。

  Sample contract
    Deployment
      ✔ Should deploy (2077ms)
    Deployment
      ✔ Should set the right owner
    mint
      ✔ user can mint (45ms)


  3 passing (2s)

参考

https://ethereum.org/en/developers/docs/smart-contracts/testing/
https://mirror.xyz/carlomigueldy.eth/9wd8ae0qkJ11MiVoOQ1vXZtZ1Yk-9RFmjK3L4of5htY

Discussion