2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スマートコントラクト開発(テスト)

Last updated at Posted at 2024-08-16

はじめに(Introduction)

EVM互換のブロックチェーンで動作するスマートコントラクトをHardhatを使って作成します。
ここではスマートコントラクトのテストを行います。

前々回前回とが終わっているものとします。

テスト作成

testフォルダにSampleToken.jsファイルを作成します。

SampleToken.js 全文
SampleToken.js
const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
const { PANIC_CODES } = require("@nomicfoundation/hardhat-chai-matchers/panic");

describe("SampleToken", function () {
    const NAME = "Sample Token";
    const SYMBOL = "STC";
    async function deployFixture() {
        const [deployer, user1, user2] = await ethers.getSigners();
        const factory = await ethers.getContractFactory("SampleToken");
        const sampleToken = await factory.deploy(NAME, SYMBOL);
        await sampleToken.waitForDeployment();
        return { sampleToken, deployer, user1, user2 };
    };
    describe("Deployment", function () {
        it("Check name and symbol", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            expect(await sampleToken.name()).to.equal(NAME);
            expect(await sampleToken.symbol()).to.equal(SYMBOL);
        });
    });
    describe("Numbers", function () {
        it("types", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            expect(await sampleToken.totalSupply()).to.equal(1000);
            expect(await sampleToken.totalSupply()).to.equal(1000n);
            expect(await sampleToken.totalSupply()).to.equal(1_000);
        });
    });
    describe("Reverted transactions", function () {
        it("reverted", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.transfer(user1.address, 1000)).to.be.reverted;
        });
        it("revertedWith", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.revertedWith(1)).to.be.revertedWith(
                "value is not zero."
            );
            await expect(sampleToken.revertedWith(1)).to.be.revertedWith(
                /value is not .*/
            );
        });
        it("revertedWithCustomError", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.transfer(user1.address, 1000)).to.be.revertedWithCustomError(
                sampleToken,
                "ERC20InsufficientBalance"
            );
            await expect(sampleToken.transfer(user1.address, 1000)).to.be.revertedWithCustomError(
                sampleToken,
                "ERC20InsufficientBalance"
            ).withArgs(
                deployer.address,
                0,
                1000
            );
        });
        it("revertedWithPanic", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 2n ** 256n - 1n);
            await expect(sampleToken.mint(user1.address, 1)).to.be.revertedWithPanic();
            await expect(sampleToken.mint(user1.address, 1)).to.be.revertedWithPanic(0x11);
            await expect(sampleToken.mint(user1.address, 1)).to.be.revertedWithPanic(
                PANIC_CODES.ARITHMETIC_OVERFLOW);
        });
        it("revertedWithoutReason", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.revertedWith(0)).to.be.revertedWithoutReason();
        });
    });
    describe("Events", function () {
        it("emit", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.emit(
                sampleToken,
                "Transfer"
            );
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.emit(
                sampleToken,
                "Transfer"
            ).withArgs(
                user1.address,
                user2.address,
                500
            );
        });
    });
    describe("Balance change", function () {
        it("changeEtherBalance", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(user1.sendTransaction({ to: user2.address, value: 1000 })).to.changeEtherBalance(
                user1.address,
                -1000
            );
        });
        it("changeTokenBalance", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.changeTokenBalance(
                sampleToken,
                user1.address,
                -500
            );
        });
        it("changeEtherBalances", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(user1.sendTransaction({ to: user2.address, value: 1000 })).to.changeEtherBalances(
                [user1.address, user2.address],
                [-1000, 1000]
            );
        });
        it("changeTokenBalances", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.changeTokenBalances(
                sampleToken,
                [user1.address, user2.address],
                [-500, 500]
            );
        });
    });
    describe("Other matchers", function () {
        it("properAddress", async function () {
            expect("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").to.be.properAddress;
        });
        it("properPrivateKey", async function () {
            expect("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80").to.be.properPrivateKey;
        });
        it("properHex", async function () {
            expect("0x1234").to.be.properHex(4);
        });
        it("hexEqual", async function () {
            expect("0x00012AB").to.hexEqual("0x12ab");
        });
    });
});

loadFixture

固定の環境を設定する関数 deployFixture を作成します。
ここでは、SampleToken をデプロイし3つの署名者を返します。

    const NAME = "Sample Token";
    const SYMBOL = "STC";
    async function deployFixture() {
        const [deployer, user1, user2] = await ethers.getSigners();
        const factory = await ethers.getContractFactory("SampleToken");
        const sampleToken = await factory.deploy(NAME, SYMBOL);
        await sampleToken.waitForDeployment();
        return { sampleToken, deployer, user1, user2 };
    };

Chai

テストはChaiで行います。
以下のテストでは、デプロイ後の namesymbol が正常に設定されているかを判定するテストです。

    describe("Deployment", function () {
        it("Check name and symbol", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            expect(await sampleToken.name()).to.equal(NAME);
            expect(await sampleToken.symbol()).to.equal(SYMBOL);
        });
    });

Hardhat Chai Matchers

Hardhatは @nomicfoundation/hardhat-chai-matchers によって、Chaiが拡張されています。
拡張された機能を使用していきます。

Numbers(数値)

EVM互換のスマートコントラクトでは大きな数値(最大 $2^{256}-1$ )数値を使います。
そのため、通常のJavaScriptで扱う数値とは異なる数値形式も有効となります。

    describe("Numbers", function () {
        it("types", async function () {
            const { sampleToken, deployer, user1, user2, user3 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            expect(await sampleToken.totalSupply()).to.equal(1000);
            expect(await sampleToken.totalSupply()).to.equal(1000n);
            expect(await sampleToken.totalSupply()).to.equal(1_000);
        });
    });

以下の数値形式に対応しています。

Reverted transactions(差戻トランザクション)

トランザクションの reverted(差戻)に関する拡張です。
意図的に reverted(差戻)を発生する為スマートコントラクトに revertedWith を追記します。

SampleToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SampleToken is ERC20, Ownable {
    constructor(
        string memory name_,
        string memory symbol_
    ) ERC20(name_, symbol_) Ownable(msg.sender) {}

    function mint(address account, uint256 value) external onlyOwner {
        _mint(account, value);
    }

    function revertedWith(uint256 value) external pure {
        require(value == 0, "value is not zero.");
        require(value == 1);
    }
}

awaitの位置が通常とは異なるので注意してください。

reverted

以下のテストでは mint されていないのに、user11000 トークンを送信しようとした為、トランザクションの差戻が発生する発生するケースです。

        it("reverted", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.transfer(user1.address, 1000)).to.be.reverted;
        });

revertedWith

次のテストでは、追記した revertedWith を使用します。
追記したとおり、"value is not zero." 文字列を付与して、トランザクションの差戻が発生する発生するケースです。
文字列と完全一致させるパターン、正規表現で一致させるパターンの2ケースを例として挙げています。

        it("revertedWith", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.revertedWith(1)).to.be.revertedWith(
                "value is not zero."
            );
            await expect(sampleToken.revertedWith(1)).to.be.revertedWith(
                /value is not .*/
            );
        });

revertedWithCustomError

次のテストでは、残高が足りない状態で user1 へトークンを送信しようとした場合のケースです。
スマートコントラクト内で定義されている、以下の ERC20InsufficientBalance が発生します。

    /**
     * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
     * @param sender Address whose tokens are being transferred.
     * @param balance Current balance for the interacting account.
     * @param needed Minimum amount required to perform a transfer.
     */
    error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);

インターフェースが定義されている sampleToken と定義されている名称 ERC20InsufficientBalance を指定します。
パラメータのチェックを行う為には withArgs を使用します。
ここでは、送信元(sender)、送信元の保持しているトークン数(balance)、送信しようとしたトークン数(needed)となります。

        it("revertedWithCustomError", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.transfer(user1.address, 1000)).to.be.revertedWithCustomError(
                sampleToken,
                "ERC20InsufficientBalance"
            );
            await expect(sampleToken.transfer(user1.address, 1000)).to.be.revertedWithCustomError(
                sampleToken,
                "ERC20InsufficientBalance"
            ).withArgs(
                deployer.address,
                0,
                1000
            );
        });

revertedWithPanic

次のテストはパニックを発生させるテストです。
トークンは uint256 で定義されている為、それ以上を設定した場合にパニックが発生します。
パニックコードは panic codeに記載されています。

テストのパターンはパニックの検知、パニックコードの指定(数値、固定値PANIC_CODES)による検出です。
ここでは、overflow を検出しています。

        it("revertedWithPanic", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 2n ** 256n - 1n);
            await expect(sampleToken.mint(user1.address, 1)).to.be.revertedWithPanic();
            await expect(sampleToken.mint(user1.address, 1)).to.be.revertedWithPanic(0x11);
            await expect(sampleToken.mint(user1.address, 1)).to.be.revertedWithPanic(
                PANIC_CODES.ARITHMETIC_OVERFLOW);
        });

revertedWithoutReason

次のテストでは、追記した revertedWith を使用します。
何の文字列も返さないパターンです。

        it("revertedWithoutReason", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(sampleToken.revertedWith(0)).to.be.revertedWithoutReason();
        });

Events

スマートコントラクトを実行した際にイベント(event)を発生する場合があります。
そのイベントについてテストする為の拡張です。

awaitの位置が通常とは異なるので注意してください。

emit

ERC20に置いてトークンを移動した場合以下のイベントが発生します。

    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

インターフェースが定義されている sampleToken と定義されている名称 Transfer を指定します。
パラメータのチェックを行う為には withArgs を使用します。
ここでは、送信元(from)、送信先(to)、送信トークン数(value)となります。

        it("emit", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.emit(
                sampleToken,
                "Transfer"
            );
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.emit(
                sampleToken,
                "Transfer"
            ).withArgs(
                user1.address,
                user2.address,
                500
            );
        });

Balance change

メインコインやトークン(ERC20)の移転に伴う残高の推移についてのテストを行う為の拡張です。

changeEtherBalance

以下のテストは、メインコインの推移についてのテストです。
user1からuser21000コイン送信したケースです。
user1の残金が-1000となっています。

        it("changeEtherBalance", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(user1.sendTransaction({ to: user2.address, value: 1000 })).to.changeEtherBalance(
                user1.address,
                -1000
            );
        });

changeTokenBalance

以下のテストは、トークン(ERC20)の推移についてのテストです。
user1からuser2500トークン送信したケースです。
user1の残金が-500となっています。
メインコインと異なり対象のトークンオブジェクト(sampleToken)を指定します。

        it("changeTokenBalance", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.changeTokenBalance(
                sampleToken,
                user1.address,
                -500
            );
        });

changeEtherBalances

以下のテストは、メインコインの推移についてのテストです。
user1からuser21000コイン送信したケースで、複数のアドレスを指定できます。
user1の残金が-1000user2の残金が1000となっています。

        it("changeEtherBalances", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await expect(user1.sendTransaction({ to: user2.address, value: 1000 })).to.changeEtherBalances(
                [user1.address, user2.address],
                [-1000, 1000]
            );
        });

changeTokenBalances

以下のテストは、トークン(ERC20)の推移についてのテストです。
user1からuser2500トークン送信したケースで、複数のアドレスを指定できます。
user1の残金が-500user2の残金が500となっています。
単発と同じく対象のトークンオブジェクト(sampleToken)を指定します。

        it("changeTokenBalances", async function () {
            const { sampleToken, deployer, user1, user2 } = await loadFixture(deployFixture);
            await sampleToken.mint(user1.address, 1000);
            await expect(sampleToken.connect(user1).transfer(user2.address, 500)).to.changeTokenBalances(
                sampleToken,
                [user1.address, user2.address],
                [-500, 500]
            );
        });

Other matchers

その他の拡張です。

properAddress

指定された文字列が適切なアドレスであることを確認します。

        it("properAddress", async function () {
            expect("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").to.be.properAddress;
        });

properPrivateKey

指定された文字列が適切な秘密鍵であることを確認します。

        it("properPrivateKey", async function () {
            expect("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80").to.be.properPrivateKey;
        });

properHex

指定された文字列が特定の長さの適切な 16 進文字列であることを確認します。

        it("properHex", async function () {
            expect("0x1234").to.be.properHex(4);
        });

hexEqual

指定された文字列の 16 進文字列が同じ数値に対応することを確認します。

        it("hexEqual", async function () {
            expect("0x00012AB").to.hexEqual("0x12ab");
        });

まとめ

Chaiの形式でテストが記述できるとともに、EVM互換のテスト用に拡張されています。
これらを使用してテストコードを記述していきます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?