メインコンテンツへスキップ

Waffleを使った動的モックアップの活用およびコントラクト呼び出しのテスト

WaffleスマートコントラクトSolidityテストモックアップ作成
中級
Daniel Izdebski
2020年11月14日
12 分の読書 minute read

チュートリアルの内容

このチュートリアルでは、以下について学びます:

  • 動的モックアップの使用方法
  • スマートコントラクト間のやりとりをテストする方法

前提知識:

  • Solidityでシンプルなスマートコントラクトを書ける
  • JavaScriptTypeScriptが扱える
  • 他のWaffleのチュートリアルを受講したか、ある程度知識がある

動的モックアップ

動的モックアップにはどのような利点があるでしょうか? まず、統合テストではなく、単体テストを書くことができるという点が挙げられます。 どういう意味かと言うと、 スマートコントラクトの依存関係について心配する必要がないので、個々のスマートコントラクトを完全に隔離した状態でテストできるのです。 それでは、その方法を具体的に見ていきましょう。

1. プロジェクト

まずはじめに、シンプルなnode.jsのプロジェクトを作成します。

mkdir dynamic-mocking
cd dynamic-mocking
mkdir contracts src
yarn init
# or if you're using npm
npm init

次に、mochaとchaiの依存関係をテストするtypescriptを追加します。

yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript
# or if you're using npm
npm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev

さらに、Waffleethersも追加します。

yarn add --dev ethereum-waffle ethers
# or if you're using npm
npm install ethereum-waffle ethers --save-dev

これにより、プロジェクトの構造は次のようになっているはずです:

1
2├── contracts
3├── package.json
4└── test

2. スマートコントラクト

動的モックアップを使用するには、依存関係を含むスマートコントラクトが必要です。 こちらで用意してありますので、ご心配なく!

今回はSolidityで書かれたシンプルなスマートコントラクトを使用しますが、このコントラクトの唯一の目的は、私たちがお金持ちであるかを確認することです。 つまり、十分なERC-20トークンを保有しているかどうかを確認するだけのスマートコントラクトです。 このコードを、./contracts/AmIRichAlready.solに追加します。

1pragma solidity ^0.6.2;
2
3interface IERC20 {
4 function balanceOf(address account) external view returns (uint256);
5}
6
7contract AmIRichAlready {
8 IERC20 private tokenContract;
9 uint public richness = 1000000 * 10 ** 18;
10
11 constructor (IERC20 _tokenContract) public {
12 tokenContract = _tokenContract;
13 }
14
15 function check() public view returns (bool) {
16 uint balance = tokenContract.balanceOf(msg.sender);
17 return balance > richness;
18 }
19}
すべて表示
コピー

動的モックアップで使用するだけなので、ERC-20全体は必要なく、関数を1つだけ持つIERC-20インターフェイスを使います。

さっそく、コントラクトをビルドしましょう! ビルドには、Waffleを使用します。 まず、コンパイルのオプションを指定するシンプルなwaffle.json設定ファイルを作成します。

1{
2 "compilerType": "solcjs",
3 "compilerVersion": "0.6.2",
4 "sourceDirectory": "./contracts",
5 "outputDirectory": "./build"
6}
コピー

さて、Waffleでコントラクトをビルドする準備が整いました。

npx waffle

簡単ですね。 build/フォルダ内に、コントラクトとインターフェイスに対応する2つのファイルが現れました。 これらのファイルを使ってテストを行います。

3. テストを実行する

実際にテストするために、AmIRichAlready.test.tsという名前のファイルを作成します。 まずはじめに、インポートを可能にしなければなりません。 これは、後ほど必要になります:

1import { expect, use } from "chai"
2import { Contract, utils, Wallet } from "ethers"
3import {
4 deployContract,
5 deployMockContract,
6 MockProvider,
7 solidity,
8} from "ethereum-waffle"

JSの依存関係の他に、ビルドしたコントラクトとインターフェイスをインポートする必要があります:

1import IERC20 from "../build/IERC20.json"
2import AmIRichAlready from "../build/AmIRichAlready.json"

Waffleでは、chaiを使ってテストを実行します。 ただしその前に、Waffleのマッチャーをchaiに追加する必要があります:

1use(solidity)

各テストの実行前にコントラクトの状態をリセットするため、beforeEach()関数を実装する必要があります。 まず、この関数には何が必要かを考えてみましょう。 コントラクトをデプロイするには、ウォレットと、AmIRichAlreadyコントラクトに引数として渡すためのデプロイされたERC-20コントラクトが必要です。

まず、ウォレットを作成します:

1const [wallet] = new MockProvider().getWallets()

次に、ERC-20コントラクトをデプロイする必要があります。 今のところ、私たちはインターフェイスしか持っていないので、工夫が必要になります。 ここで、Waffleが助けてくれます。 Waffleには、インターフェイスのABIだけを使用してコントラクトを作成できる、魔法のようなdeployMockContract()関数が含まれているのです。

1const mockERC20 = await deployMockContract(wallet, IERC20.abi)

ウォレットとデプロイされたERC-20コントラクトの両方が準備できたので、さっそくAmIRichAlreadyコントラクトをデプロイしましょう:

1const contract = await deployContract(wallet, AmIRichAlready, [
2 mockERC20.address,
3])

これで、beforeEach()関数が作成できました。 現在のところ、 AmIRichAlready.test.tsのファイルは以下のようになっているはずです:

1import { expect, use } from "chai"
2import { Contract, utils, Wallet } from "ethers"
3import {
4 deployContract,
5 deployMockContract,
6 MockProvider,
7 solidity,
8} from "ethereum-waffle"
9
10import IERC20 from "../build/IERC20.json"
11import AmIRichAlready from "../build/AmIRichAlready.json"
12
13use(solidity)
14
15describe("Am I Rich Already", () => {
16 let mockERC20: Contract
17 let contract: Contract
18 let wallet: Wallet
19
20 beforeEach(async () => {
21 ;[wallet] = new MockProvider().getWallets()
22 mockERC20 = await deployMockContract(wallet, IERC20.abi)
23 contract = await deployContract(wallet, AmIRichAlready, [mockERC20.address])
24 })
25})
すべて表示

このAmIRichAlreadyコントラクトの最初のテストを書きましょう。 テストの対象は何にすべきだと思いますか? 言うまでもなく、 私たちがお金持ちであるかをチェックするのですね :)

でもちょっと待ってください。 今作成したコントラクトでは、どのような値を返すべきかを指定していませんでした。 balanceOf()関数のロジックが実装されていないからです。 ここでもWaffleが助けてくれます。 モックアップを作成したコントラクトには、新たに以下のような素敵なコードが付け加えられています:

1await mockERC20.mock.<nameOfMethod>.returns(<value>)
2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)

これを知っていれば、最初のテストを書くことができます。

1it("returns false if the wallet has less than 1000000 tokens", async () => {
2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))
3 expect(await contract.check()).to.be.equal(false)
4})

このテストを、構成要素に分解してみましょう:

  1. モックアップのERC-20コントラクトは、常に、999999トークンの残高を返すように設定します。
  2. contract.check()メソッドが、falseを返すか確認します。

ようやく、テストを実行する準備ができました。

1つのテストが合格

テストは実行されましたが・・・改善の余地がありますね。 balanceOf()関数は、常に99999を返します。 実際のコントラクトのように、関数が値を返すウォレットを指定するともっとテストらしくなるでしょう。

1it("returns false if the wallet has less than 1000001 tokens", async () => {
2 await mockERC20.mock.balanceOf
3 .withArgs(wallet.address)
4 .returns(utils.parseEther("999999"))
5 expect(await contract.check()).to.be.equal(false)
6})

このテストでは、今のところ、私たちが十分にお金持ちではない場合のみをチェックしています。 次に、その反対もチェックできるようにしてみましょう:

1it("returns true if the wallet has at least 1000001 tokens", async () => {
2 await mockERC20.mock.balanceOf
3 .withArgs(wallet.address)
4 .returns(utils.parseEther("1000001"))
5 expect(await contract.check()).to.be.equal(true)
6})

テストを実行します・・・

2つのテストが合格

そして・・・うまく行きました! 私たちのコントラクトは、意図したとおりに動作しているようです :)

コントラクトの呼び出しをテストする

これまでの進展をまとめておきましょう。 AmIRichAlreadyコントラクトの機能をテストし、正常に動作していることが確認できたようです。 これで終わりだろうって? いいえ、まだ少し残っています。 Waffeを使えば、さらに多くの事項をテストすることができます。 具体的に説明すると、 WaffleにはcalledOnContract()マッチャーとcalledOnContractWith()マッチャーが搭載されています。 これらを使えば、作成したコントラクトがモックアップのERC-20コントラクトを呼び出したかどうかを確認できるのです。 いずれかのマッチャーを使用した基本的なテストは、次のようになります:

1it("checks if contract called balanceOf on the ERC20 token", async () => {
2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))
3 await contract.check()
4 expect("balanceOf").to.be.calledOnContract(mockERC20)
5})

もう一方のマッチャーを使用することで、さらにこのテストを充実させることができます:

1it("checks if contract called balanceOf with certain wallet on the ERC20 token", async () => {
2 await mockERC20.mock.balanceOf
3 .withArgs(wallet.address)
4 .returns(utils.parseEther("999999"))
5 await contract.check()
6 expect("balanceOf").to.be.calledOnContractWith(mockERC20, [wallet.address])
7})

テストがうまく行ったか確認しましょう:

3つのテストが合格

幸いなことに、すべてのテストに合格しました。

Waffleを使えば、コントラクトの呼び出しをとても簡単にテストできます。 特にすばらしいのは、 これらのマッチャーを通常のコントラクトとモックアップのコントラクトの両方に使えることです! 他のテクノロジー向けの人気が高いテストライブラリと同じように、Waffleは、コードを挿入するのではなく、EVM呼び出しを記録し、フィルタ処理を行うアプローチを採用しています。

おわりに

おめでとうございます! これで、Waffleを使用して、コントラクトの呼び出しや、モックアップのコントラクトを動的にテストする方法を身に付けることができました。 この他にもたくさんの興味深い機能がありますので、 Waffleのドキュメンテーションに目を通すことをおすすめします。

Waffleのドキュメンテーションは、こちら(opens in a new tab)から入手できます。

このチュートリアルのソースコードは、こちら(opens in a new tab)からアクセスできます。

さらに、以下のチュートリアルをおすすめします:

  • Waffleを使ってスマートコントラクトをテストする

最終編集者: @nhsz(opens in a new tab), 2024年2月27日

このチュートリアルは役に立ちましたか?