こんにちは!今日は、スマートコントラクトのテストについて深掘りしてみましょう。EthereumのSolidityで書かれたスマートコントラクトが意図通りに動作していることを確認するためのテストの書き方を、具体的な手順と共に見ていきます。
テストするコードはこの記事1でも解説しています。
1. 環境構築
まずは、テストのための環境構築から始めましょう。Node.jsのバージョン管理ツールであるnvmを用いて、適切なバージョンのNode.jsをインストールします。
nvm install 20
nvm use 20
このコマンドは、Node.jsのバージョン20 (LTS)をインストールし、使用するバージョンとして設定します。バージョンの一貫性は、複数の環境で一貫した動作を保証するために重要です。
次に、新規プロジェクトのディレクトリを作成し、Node.jsプロジェクトを初期化します。
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init
この時点で、package.jsonファイルが生成され、プロジェクトの依存関係を管理する準備が整います。
npm install --save-dev hardhat
HardhatはEthereumの開発に特化したJavaScriptフレームワークで、スマートコントラクトのコンパイル、テスト、デプロイなどを手助けします。

npm install --save-dev @nomicfoundation/hardhat-toolbox
ここではHardhat Toolboxもインストールします。これによりネットワーク関連のヘルパー関数など、Ethereum開発をより容易にする一連のツールを利用できます。
2. Solidityスマートコントラクトのコンパイル
npx hardhat compile
コードが完成したら、そのコードをEthereumネットワークが理解できる形にコンパイルします。npx hardhat compileコマンドにより、スマートコントラクトがEVM(Ethereum Virtual Machine)バイトコードに変換されます。
3. テストコードの解説
テストの実行には、テストフレームワークのMochaとアサーションライブラリのChaiを使用します。
const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
ここでは、必要なライブラリをインポートします。loadFixtureはテスト環境のセットアップに使い、expectはテストの結果が予期したものかチェックするための関数です。
describe("Token contract", function () {
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const hardhatToken = await ethers.deployContract("Token");
return { hardhatToken, owner, addr1, addr2 };
}
describeはテストスイートを定義します。これは関連するテストケースを一つのグループとしてまとめるものです。この中でdeployTokenFixture関数を定義します。この関数は、テストの前準備としてスマートコントラクトをデプロイし、必要なアカウントを取得します。
このテストスイート内でトークンコントラクトがデプロイされている箇所は deployTokenFixture 関数内です。この関数の中で、const hardhatToken = await ethers.deployContract("Token"); という行でトークンコントラクトがデプロイされています。
この行では、ethers ライブラリを使ってスマートコントラクトをデプロイしています。ここで "Token" という名前のコントラクトがデプロイされています。
通常、Ethers.js または Hardhat を使ってスマートコントラクトをデプロイする場合、デプロイを行うアカウントは getSigners メソッドで取得した一連の署名者の中から選ばれます。このテストスイートでは、const [owner, addr1, addr2] = await ethers.getSigners(); という行で署名者を取得しています。
この owner がデプロイしたアカウントとなり、デプロイされたスマートコントラクトの「オーナー」または「管理者」の役割を持つことが一般的です。コントラクトのデプロイメント時にコントラクトの「オーナー」または「管理者」を設定するかどうかは、コントラクトのソースコードによります。
それが具体的にどのように行われているかを知るためには、"Token"という名前のスマートコントラクトのソースコードを確認する必要があります。通常、このような役割の設定はコントラクトのコンストラクター関数内で行われます。
ちょっと待って、Fixtureって何?
loadFixtureを使うのと使わないのとではどう違うの?
loadFixture
loadFixtureはHardhatのスナップショット機能を使って共通のセットアップ(またはフィクスチャ)をテスト間で共有します。具体的には、loadFixtureは与えられた関数(ここではdeployTokenFixture)を呼び出しますが、関数が初めて呼び出されるときには通常通り実行し、その後の呼び出しではネットワークのスナップショット(特定の時点でのブロックチェーンの状態をキャプチャし、保存する機能です。一度スナップショットが取られると、その時点の状態に戻すことが可能になります。)を利用します。スナップショットを利用することで、テストの初期状態をリセットするための時間が大幅に節約でき、テストがより高速になります。さらに、テストの隔離も確保され、各テストが他のテストの結果に影響を与えないようになります。
deployTokenFixture
直接deployTokenFixtureを呼び出すと、その都度新しいコントラクトがデプロイされ、スナップショット機能は使われません。これは、それぞれのテストケースで新たにコントラクトをデプロイする時間がかかり、テスト速度が低下することを意味します。また、テストケース間での状態の共有も行われません。
具体的な例
例えば、あなたが10の異なるテストケースを持ち、それぞれでコントラクトをデプロイしたいとします。それぞれのテストでdeployTokenFixtureを直接呼び出すと、コントラクトのデプロイは10回行われ、それぞれのテストケースでブロックチェーンの状態がリセットされます。これには時間がかかり、テストスピードが低下します。
一方で、loadFixture(deployTokenFixture)を使用すると、deployTokenFixtureは最初のテストケースで一度だけ実行され、その結果が後続のテストケースで再利用されます。これにより、テストスピードが大幅に向上します。また、各テストケースはスナップショットから新たにスタートするので、互いに影響を与えることなく独立して実行することができます。
したがって、テストの速度を向上させ、各テストを隔離したい場合は、loadFixtureを使用することをお勧めします。
それではテストスイートの説明に戻ります。
describe("Deployment", function () {
it("Should set the right owner", async function () {
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
expect(await hardhatToken.owner()).to.equal(owner.address); //ここで最初に`deployTokenFixture()`でスマートコントラクトをデプロイしたアカウントと、今からテストで使うアカウントが同じかどうかをテストします。
});
hardhatToken.owner()はデプロイしたスマートコントラクトが保持する「オーナー」または「管理者」のアドレスを返す関数です。この「オーナー」はスマートコントラクトのデプロイメント時に通常は設定され、特定の機能(例えばコントラクトの更新や状態変更など)を制御する役割を持ちます。
一方、owner.addressはテストの設定時に取得したEthereumアドレスを表します。ここでのownerはawait ethers.getSigners()から取得される一連の署名者(アドレス)の最初のものを指します。このownerはテスト実行者のアカウントを表し、このアカウントからスマートコントラクトがデプロイされます。
したがって、expect(await hardhatToken.owner()).to.equal(owner.address);というテストは、スマートコントラクトがデプロイされた際に設定された「オーナー」が、期待通りテストを実行したアカウント(つまりowner.address)であることを確認しています。このテストが成功すると、スマートコントラクトが正しくデプロイされ、適切なアドレスにオーナーの権限が与えられていることが確認できます。
it("Should assign the total supply of tokens to the owner", async function () {
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});
このテストでは、ownerが持っている総額(totalSupply)がbalanceOf(owner.address)で求めた額と一致しているべきだよね、っていうことを確認するためのテストです。
it("Should transfer tokens between accounts", async function () {
const { hardhatToken, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await expect(hardhatToken.transfer(addr1.address, 50)).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);
await expect(hardhatToken.connect(addr1).transfer(addr2.address, 50)).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
});
});
こちらのテストでは、トークンが正しく送金されるかを検証します。
changeTokenBalancesの使い方についてはこちらにhardhatオフィシャルドキュメントのスクショを貼っておきますね。

4. テストの実行
最後に、テストを実行します。
npx hardhat test
このコマンドでテストスイート全体が実行され、期待通りの結果が得られるかどうかが確認できます。

以上が、Solidityで書かれたEthereumのスマートコントラクトのテストを行う一例です。テストは信頼性の高いスマートコントラクトを開発するための重要なステップです。ぜひ活用してみてくださいね!