前回は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できないこと
等々、セール日までにテストすべきテストケースが山積みです。
余裕のある方は、↑のテストも書いてみると良いです。
次のセクションへ
これで
- セットアップ処理を共通化
- セットアップ処理を使った上で、テストを追加
ができました。
次のセクションが最後です。
最後は、テストコードを使ってデバッグしながら開発する手法をご紹介します。
コメント