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

Waffleライブラリを使用したシンプルなスマートコントラクトのテスト

スマートコントラクトSolidityWaffleテスト
初級
Ewa Kowalska
2021年2月26日
9 分の読書 minute read

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

  • ウォレットの残高が変わることのテスト
  • 特定の引数でイベントが発行されることのテスト
  • トランザクションが取り消されたことのアサーション

前提知識

  • 新規のJavaScriptまたはTypeScriptのプロジェクトを作成できる
  • JavaScriptのテストの基本的な経験がある
  • yarnやnpmなどのパッケージマネージャーを使用したことがある
  • スマートコントラクトおよびSolidityのごく基本的な知識を持っている

はじめに

このチュートリアルでは、yarnを使ってテストのセットアップおよび実行をしていますが、npmの方が良ければnpmでも問題ありません。公式のWaffleのドキュメントは、こちら(opens in a new tab)になります。

依存関係のインストール

プロジェクトに対してethereum-waffleとtypescriptの依存関係を開発環境の依存関係に追加(opens in a new tab)します。

yarn add --dev ethereum-waffle ts-node typescript @types/jest

スマートコントラクトのコード例

このチュートリアルでは、EtherSplitterというシンプルなスマートコントラクトの例に取り組みます。 このコードでは、誰もがweiを送信でき、それを2つの事前定義された受信者間で均等に分割するだけです。 split関数ではweiの数が偶数でなければなりません。さもないと処理が取り消されます。 両方の受信者に対して、weiの送金を実行し、続いてTransferイベントを発行します。

src/EtherSplitter.solに、EtherSplitterコードのスニペットを配置します。

1pragma solidity ^0.6.0;
2
3contract EtherSplitter {
4 address payable receiver1;
5 address payable receiver2;
6
7 event Transfer(address from, address to, uint256 amount);
8
9 constructor(address payable _address1, address payable _address2) public {
10 receiver1 = _address1;
11 receiver2 = _address2;
12 }
13
14 function split() public payable {
15 require(msg.value % 2 == 0, 'Uneven wei amount not allowed');
16 receiver1.transfer(msg.value / 2);
17 emit Transfer(msg.sender, receiver1, msg.value / 2);
18 receiver2.transfer(msg.value / 2);
19 emit Transfer(msg.sender, receiver2, msg.value / 2);
20 }
21}
すべて表示
コピー

コントラクトのコンパイル

コントラクトをコンパイル(opens in a new tab)するのに、package.jsonファイルに次のエントリを追加します。

1"scripts": {
2 "build": "waffle"
3 }
コピー

次に、プロジェクトのルートディレクトリにWaffleの設定ファイル (waffle.json) を作成し、次の設定を貼り付けます。

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

yarn buildを実行してください。 終了すると、buildディレクトリに、JSON形式でコンパイルされたEtherSplitterコントラクトが現れます。

テストの設定

Waffleでテストするには ChaiマッチャーとMochaが必要になるため、プロジェクトに追加(opens in a new tab)します。 次のようにscriptの場所にtestエントリを追加してpackage.jsonファイルを更新してください。

1"scripts": {
2 "build": "waffle",
3 "test": "export NODE_ENV=test && mocha -r ts-node/register 'test/**/*.test.ts'"
4 }
コピー

テストを実行(opens in a new tab)する場合は、 yarn testを実行します。

テストを実行する

それでは、testディレクトリを作成し、新しいファイル test\EtherSplitter.test.tsを作成してください。 以下のスニペットをコピーして、テストファイルに貼り付けてください。

1import { expect, use } from "chai"
2import { Contract } from "ethers"
3import { deployContract, MockProvider, solidity } from "ethereum-waffle"
4import EtherSplitter from "../build/EtherSplitter.json"
5
6use(solidity)
7
8describe("Ether Splitter", () => {
9 const [sender, receiver1, receiver2] = new MockProvider().getWallets()
10 let splitter: Contract
11
12 beforeEach(async () => {
13 splitter = await deployContract(sender, EtherSplitter, [
14 receiver1.address,
15 receiver2.address,
16 ])
17 })
18
19 // add the tests here
20})
すべて表示

始める前に、少し解説をします。 MockProviderは、ブロックチェーンのモックバージョンを作成します。 また、EtherSplitterコントラクトのテストで役立つモックウォレットも提供します。 このプロバイダーで、getWallets()メソッドを呼び出すと最大10個までウォレットを取得することができます。 この例では、3つのウォレットを取得します。1つは、送信者用で、2つは、受信者用です。

次に、「splitter」という変数を宣言します。これは、 EtherSplitterコントラクトのモックです。 このモックは、単一のテストを実行する前にdeployContractメソッドによって作成されます。 当該のメソッドは、最初のパラメータとして渡されたウォレット (この場合は送信者のウォレット) からコントラクトのデプロイメントをシミュレートします。 2番目のパラメータは、テストされるコントラクトのABIとバイトコードです。コンパイルされたEtherSplitterコントラクトのjsonファイルをbuildディレクトから渡します。 3番目のパラメータは、コントラクトのコンストラクタ引数を持つ配列です。この場合、受信者の2つのアドレスです。

changeBalances

まず、splitメソッドによって実際に受取人のウォレットの残高が変わるかどうかを確認します。 送信者のアカウントから50weiを分割すると、両方の受信者の残高が25wei増えることが期待されます。 ここで、WaffleのchangeBalancesマッチャーを使います。

1it("Changes accounts balances", async () => {
2 await expect(() => splitter.split({ value: 50 })).to.changeBalances(
3 [receiver1, receiver2],
4 [25, 25]
5 )
6})

マッチャーの最初のパラメータとして、受信者のウォレットの配列を渡し、2番目のパラメータとして、対応するアカウントで予想される増加分を配列で渡します。 特定のウォレットの残高を確認したい場合は、以下の例のように、配列を渡さなくてもchangeBalanceマッチャーを使えます。

1it("Changes account balance", async () => {
2 await expect(() => splitter.split({ value: 50 })).to.changeBalance(
3 receiver1,
4 25
5 )
6})

changeBalancechangeBalancesのどちらの場合も、split関数をコールバックとして渡します。マッチャーは呼び出しの前後に残高の状態にアクセスする必要があるためです。

次では、weiの各転送後にTransferイベントが発行されたかどうかをテストします。 それでは、Waffleの別のマッチャーに移ります。

Emit

1it("Emits event on the transfer to the first receiver", async () => {
2 await expect(splitter.split({ value: 50 }))
3 .to.emit(splitter, "Transfer")
4 .withArgs(sender.address, receiver1.address, 25)
5})
6
7it("Emits event on the transfer to the second receiver", async () => {
8 await expect(splitter.split({ value: 50 }))
9 .to.emit(splitter, "Transfer")
10 .withArgs(sender.address, receiver2.address, 25)
11})
すべて表示

emitマッチャーを使うと、メソッドの呼び出し時にコントラクトがイベントを発行したかどうかを確認できます。 emitマッチャーへのパラメーターとして、イベントを発行することが予期されるモックコントラクトとそのイベントの名前を渡します。 この場合、モックコントラクトはsplitterで、イベント名はTransferです。 また、イベントの発行で引数の正確な値を検証することもできます。その場合、イベントの宣言で期待される数の引数を withArgsマッチャーに渡します。 EtherSplitterコントラクトの場合では、送金されるwei単位の金額とともに送信者と受信者のアドレスを渡します。

revertedWith

最後の例として、weiの数が奇数の場合にトランザクションが取り消されるかどうかを確認します。 ここでは、revertedWithマッチャーを使います。

1it("Reverts when Vei amount uneven", async () => {
2 await expect(splitter.split({ value: 51 })).to.be.revertedWith(
3 "Uneven wei amount not allowed"
4 )
5})

このテストをパスすれば、トランザクションが実際に取り消されたことが保証されます。 ただし、requireステートメントで渡したメッセージと、revertedWithで期待しているメッセージとが完全に一致している必要があります。 EtherSplitterコントラクトのコードに戻った場合、weiの金額のrequireステートメントで、「偶数でないwei単位の金額は許可されていません」というメッセージが表示されます。 これは、テストで予期されるメッセージと一致します。 それらが等しくなければ、テストは失敗します。

おめでとうございます!

Waffleでスマートコントラクトをテストするための最初の大きな一歩を踏み出すことができました。 他のWaffleのチュートリアルについては、以下をご参照ください。

  • Waffleを使って、ERC-20をテストする
  • Waffleを使った動的モックアップの活用およびコントラクト呼び出しのテスト
  • WaffleでHardhatとethersを使って「Hello world!」と出力するチュートリアル

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

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