TruffleによるDapp開発のテストについて

はじめに

  • EthereumによるDapp開発では、TruffleGanacheといった開発ツールを利用できる。
  • スマートコントラクトは、一度ブロックチェーンに取り込まれると変更できないため、テストを事前に実施すべきである。
  • とは言っても、ETHの支払いや時間の進め方など、スマートコントラクトならではのテストをどのように書くべきかわからなかった。
  • そこで今回は、Truffle Ganache環境における、スマートコントラクトのテスト方法について調べた内容を記載する。
  • 動作確認バージョンは、以下の通り。
    • Truffle : 4.1.14
    • Ganache : 1.1.0

Solidityのテストコードで利用可能な言語

Truffleのドキュメントには、テストコードで利用可能な言語として以下を挙げている。

  • Solidity
    • 特徴は、依存ライブラリが必要なく、コントラクトのテストが可能なこと。
  • Javascript
    • 特徴は、依存ライブラリが必要だが、実際のクライアントを想定した接続テストが可能なこと。
    • Truffleは、以下に依存している。

以降では、Javascriptによるテストコードの書き方について説明する。

テストコードの書き方

テスト対象のコード

ブロックチェーンの入門書でよくあるクラウドファンディングコントラクトをテスト対象とする。 このコントラクトでは、以下の処理が行える。(セキュアでない処理があるがサンプルということで...)

  • 資金の調達者(Owner)が、「目標金額(通貨:ETH)」と、「締め切り期日」を設定できる。
  • Ownerが、期日までに援助者(Investor)から目標金額を獲得できたら、Ownerに全額資金を送付できる。
  • Ownerが、期日までに援助者(Investor)から目標金額を獲得できなかったら、Investorに資金を返済できる。

CrowdFunding.sol

pragma solidity ^0.4.23;

contract CrowdFunding {

  struct Investor {
    address addr;
    uint amount;
  }

  address public owner;
  uint public numInvestors;
  uint public deadline;
  string public status;
  bool public ended;
  uint public goalAmount;
  uint public totalAmount;
  mapping (uint => Investor) public investors;

  event Fund(address indexed investor, uint amount);
  event CheckGoalReached(address indexed owner);

  modifier onlyOwner () {
    require(msg.sender == owner);
    _;
  }

  constructor(uint _duration, uint _goalAmount) public {
    owner = msg.sender;
    deadline = now + _duration;
    goalAmount = _goalAmount;
    status = "Funding";
    ended = false;
    numInvestors = 0;
    totalAmount = 0;
  }

  function fund() public payable {
    require(!ended);

    Investor storage inv = investors[numInvestors++];
    inv.addr = msg.sender;
    inv.amount = msg.value;
    totalAmount += inv.amount;

    emit Fund(msg.sender, msg.value);
  }

  function checkGoalReached() public onlyOwner {
    require(!ended);

    require(now >= deadline);
    if(totalAmount >= goalAmount) {
      status = "Campaign Succeeded";
      ended = true;
      if(!owner.send(address(this).balance)) {
        revert("Failed to send the balance to the owner");
      }
    } else {
      uint i = 0;
      status = "Campaign Failed";
      ended = true;

      while(i <= numInvestors) {
        if(!investors[i].addr.send(investors[i].amount)) {
          revert("Failed to send the balance to investors");
        }
        i++;
      }
    }
    emit CheckGoalReached(msg.sender);
  }

  function kill() public onlyOwner {
    selfdestruct(owner);
  }
}

1. 基本的なテスト

  • mochaで一般的に使われるdescribe()の代わりにcontract()を使う。
  • contractでは、addressの配列がパラメータとして与えられる。
    • [owner, investor1, investor2]のようにテスト実行時に使われる各アドレスの役割を変数名にするとわかりやすい。
  • artifacts.require()でテスト対象のコントラクトの型を宣言する。
  • web3が利用可能なので、web3.eth.getBalanceといった処理が書ける。
  • テストコードは非同期処理として記載するので async/awaitを利用するとネストが少なくすっきり書ける。
const BigNumber = web3.BigNumber

require('chai')
  .use(require('chai-as-promised'))
  .use(require('chai-bignumber')(BigNumber))
  .should();

var CrowdFunding = artifacts.require("CrowdFunding");

contract('CrowdFunding', ([owner, investor1, investor2]) => {
  const DURATION = 1800; // 30 minutes
  const GOAL_AMOUNT = new web3.BigNumber(web3.toWei(1, 'ether'));

  let instance;

  beforeEach(async () => {
    instance = await CrowdFunding.new(DURATION, GOAL_AMOUNT, { from: owner });
  });

  it('should be Funding state initially', async () => {
    (await instance.status()).should.equal('Funding');
    (await instance.ended()).should.be.false;
  });
});

2. 例外処理のテスト

  • chai-as-promisedのrejectedWithを利用してerrorメッセージ文字列にrevertが含まれているかをチェックする。
  it('should fail if checkGoalReaced is called before campaign end', async() => {
    instance.checkGoalReached({ from: owner }).should.be.rejectedWith('revert');
  });

3. 時間を進めたい場合のテスト

  • Ganacheに対して、jsonrpc経由で非標準メソッド evm_increaseTime を呼び出す。
  • その他の非標準メソッドとして以下がある。
    • evm_snapshot, evm_revert, evm_increaseTime, evm_mine
  it('should fail if fund is called after the campaign end', async () => {
    const amount1 = new web3.BigNumber(web3.toWei(1, 'ether'));
    await instance.fund({ from: investor1, value: amount1 });
    await increaseTime(duration.hours(1));
    await instance.checkGoalReached({ from: owner });
    instance.fund({ from: investor1, value: amount1 }).should.be.rejectedWith('revert');
  });

increaseTime.js

export const duration = {
  seconds: function (val) { return val; },
  minutes: function (val) { return val * this.seconds(60); },
  hours: function (val) { return val * this.minutes(60); },
  days: function (val) { return val * this.hours(24); },
  weeks: function (val) { return val * this.days(7); },
  years: function (val) { return val * this.days(365); },
};

export function increaseTime(duration) {
  return new Promise((resolve, reject) => {
    web3.currentProvider.sendAsync({
      jsonrpc: '2.0',
      method: 'evm_increaseTime',
      params: [duration],
      id: Date.now(),
    }, err => {
      if (err) return reject(err)
        resolve()
    })
  })
};

4. Eventのテスト

  • 原因不明であるが、私の環境では、初回呼び出しのみイベントが2回発火されてしまう。
  • event.watchの結果を確認するよりも、event.getで呼ばれた回数を確認する方法をとっている。
  it('should fire an event after calling fund', async () => {
    const amount1 = new web3.BigNumber(web3.toWei(0.1, 'ether'));
    const event = instance.Fund({}, {fromBlock: 0, toBlock: 'latest'});

    // event.watch(function (error, result) {
      // if (!error) {
        // console.log(result.args); // sometimes an event is called twice
        // result.args['investor'].should.equal(investor1);
        // result.args['amount'].should.be.bignumber.equal(amount1);
      // } else {
        // assert.fail('error occured');
      // }
    // });

    await instance.fund({ from: investor1, value: amount1 });
    await instance.fund({ from: investor2, value: amount1 });
    await instance.fund({ from: investor1, value: amount1 });
    assert.equal(3, event.get().length)
  });

5. ETH支払のテスト

  • web3.ethを利用してトランザクション前後で消費したETHを確認する。
  • ETH支払いにはガスがかかるため、トランザクションから消費したガスを取得し、そのガスを考慮した計算が必要。
  it('should success the campaign if totalAmount is reached by deadline, then send balance to owner', async () => {
    const amount1 = new web3.BigNumber(web3.toWei(1, 'ether'));
    const pre = web3.eth.getBalance(owner);
    await instance.fund({ from: investor1, value: amount1 });
    await increaseTime(duration.hours(1));
    const res = await instance.checkGoalReached({ from: owner });
    const gasCost = getTransactionGasCost(res['tx']);
    const post = web3.eth.getBalance(owner);
    post.minus(pre).plus(gasCost).should.be.bignumber.equal(amount1);
    (await instance.status()).should.be.equal("Campaign Succeeded");
    (await instance.ended()).should.be.true;
    (await web3.eth.getBalance(instance.address)).should.bignumber.equal(0);
  });
export function getTransactionGasCost(tx) {
  const transaction = web3.eth.getTransactionReceipt(tx);
  const amount = transaction.gasUsed;
  const price = web3.eth.getTransaction(tx).gasPrice;

  return new web3.BigNumber(price * amount);
}

ソースコード

今回のサンプルコードは、Githubに格納している。

参考

//

My Cheat

Markdown

Markdown Sample - Trial and error

Slack

Action Command
クイックスイッチャー Command + K
スレッド画面を開く Command + Shift + T
右サイトバーを開く/閉じる Command + ,
アクティビティ@ Command + Shift + M
スター付きアイテムを表示 Command + Shift + S
現在開いているチャンネルや会話の中で検索 Command + F
メッセージを未読にする Option + Click the message
自分の最後のメッセージを編集 Command + ↑

Google Docs

Action Command
箇条書き Command + Shift + 8

tmux

https://qiita.com/vintersnow/items/be4b29652ff665c45198

Vim

Category Function Command
基本 編集中のファイルの場所をカレントディレクトリにする :cd %:h
基本 インデント挿入 >>
基本 インデント削除 <<
基本 インデント挿入(インサートモード) Ctrl + t
基本 インデント削除(インサートモード) Ctrl + d
折りたたみ 折りたたみの有効無効きりかえ zi
折りたたみの作成 zf
折りたたみの開け閉め za
折りたたみを削除 zd
置換 構文 :[範囲]S/[置換前文字列]/[置換後文字列]/[オプション]
ページ全体を確認ありで置換 :%s/from/to/gc
インデント 現在行のインデント ==
指定範囲のインデント =[範囲]
現在行を左にインデント >
現在行を右にインデント <
Surround Vim 文を囲んでいる'を消す ds' (delete surround)
'で囲まれた部分を消す di' (delete inside)
'を"に変更 cs'" (change surround)
'で囲まれた部分を消して、インサートモードに入る ci' (change inside)
ビジュアルモードで選択した部分を'で囲む S'
文を'で囲む yss' (yank surround sentence)
カーソルがある単語を'で囲む ysiw' (yank surround inner word)
イカスタマイズ インサートモードで移動 Ctrl + j, k, h, l
最後に編集された位置に移動 gb
カーソル位置の単語をyank vy
ビジュアルモードで行末まで選択 v
対応する括弧に移動 (
NERDTreeオンオフ Ctrl + e
NERDTree 隠しファイル表示 I
NERDTree ファイル追加 m
NERDTree 親ディレクトリに移動 u
NERDTree ヘルプ ?
Unite 全部乗せ ,fu
Unite ファイル一覧 ,ff
Unite バッファ一覧 ,fb
Unite 常用セット ,fu
Unite 最近使用したファイル一覧 ,fm
Unite 現在のバッファのカレントディレクトリからファイル一覧 ,fd
カーソル下のURLをブラウザで開く ,o
カーソル下の語をググる ,g
選択した文字列を検索 //
選択した文字列を置換 /r
ヘルプ Ctrl + i
カーソル下のキーワードをヘルプでひく Ctrl + i, Ctrl + i
XMLファイルをフォーマットする :Formatxml
.vimrcの編集 :Ev
.vimrcの反映 :Rv
指定文字コードでファイルを開く :Cp932, :Eucjp, :Iso2022jp, :Utf8, :Jis, :Sjis

Vimの使い方 よく使うコマンドまとめ | Memo on the Web

画面分割

easy-align-plugin

1. キー
2. オプション: Enterキーを押すごとにどちらに揃えるか (left, right,or center)
3. オプション: 何番目のデリミタか (default: 1)
 * 1: 1番目のデリミタ
 * 2: 2番目のデリミタ
 * *: すべてのデリミタ
 * **: すべてのデリミタ (左揃え、右揃えを交互に)
 * -: 後ろから数えて1番目のデリミタ(-1)
 * -2: 最後から数えて2番目のデリミタ
4. デリミタキー (, =, :, ., |, &, #, ,)
  • 1番目の [space] で左揃え [Enter][space]
Paul   McCartney 1942 May
George Harrison 1943 April
Ringo  Starr 1940 June
Pete   Best 1941 January
  • 2番目の[space]で左揃え [Enter]2[space]
Paul McCartney  1942 May
George Harrison 1943 April
Ringo Starr     1940 June
Pete Best       1941 January
  • すべての[space]で左揃え [Enter]*[space]
Paul   McCartney 1942 May
George Harrison  1943 April
Ringo  Starr     1940 June
Pete   Best      1941 January
  • すべての[space]で左揃え右揃えを交互に [Enter]**[space]
Paul   McCartney 1942     May
George  Harrison 1943   April
Ringo      Starr 1940    June
Pete        Best 1941 January
  • 後ろから2番目の[space]で右揃え [Enter][Enter]-2[space]

Paul McCartney 1942 May George Harrison 1943 April Ringo Starr 1940 June Pete Best 1941 January

-すべての[space]で中央揃え [Enter][Enter][Enter]*[space]

 Paul  McCartney 1942   May
George Harrison  1943  April
Ringo    Starr   1940  June
 Pete    Best    1941 January

Junit

JUnit・JMockitチートシート - Qiita

git

Command Description Example Note
git reset 直前のコミットを取り消す git reset --soft HEAD^ 作業ツリーに加えた変更点まで取り消したい場合は--hard
git revert 作業ツリーを指定したコミット時点の状態に戻す git revert <コミット名> git resetと違い作業ツリーを差し戻した情報が作業履歴に残る
git stash 現在の作業ツリーの状態を一時的に保管する git stash <保存名もしくはコメントなど> 作業ツリーを再度呼び出すには git stash pop, 一時保存されている作業ツリー一覧を見るには git stash list
git rebase ブランチの派生元(上流)を変更する git rebase <派生元ブランチ>

IntelliJ IDEA

Function Command(Mac) Command(Windows)
ビューを開く(*1) Command + #[0-9] Alt + #[0-9]
設定ダイアログ Command + , Ctrl + Alt + S
プロジェクト構造ダイアログ Command + ; Ctrl + Alt + Shift + S
アクションを探す Command + Shift + A Ctrl + Shift + A
どこでも検索 Double Shift Double Shift
最近のファイルをポップアップ Command + E Ctrl + E
クラスに移動 Command + O Ctrl + N
ファイルに移動 Command + Shift + O Ctrl + Shift + N
宣言に移動 Command + B Ctrl + B
ファイル構造ポップアップ Command + F12 Ctrl + F12
呼び出し階層 Ctrl + Option + H Ctrl + Alt + H
エディタ閉じる Command + W
実行/デバッグ Ctrl + R/D Shift + F10/F9
コードブロックを展開/折りたため Command + +/- Ctrl + +/-
すべて展開 Command + Shift + + Ctrl + Shift + +
すべて折りたたむ Command + Shift + - Ctrl + Shift + -
インポート文の整理 Ctrl + Option + O Ctrl + Alt + O
フォーマット Command + Alt + L Ctrl + Alt + L
戻る Command + Alt + ← Ctrl + Alt + ←
進む Command + Alt + → Ctrl + Alt + →

(*1)

  • 1: Project
  • 2: Favorite
  • 3: Find
  • 4: Run
  • 5: Debug
  • 6: ToDo
  • 7: Structure
  • 8: Version Control
  • Option + F12: Terminal
  • F12: まえのウィンドウに戻る
  • Esc: エディタに移動

Eclipse

Function Command(Mac) Command(Windows)
クイックフィックス command + 1 Ctrl + 1
コメントアウト(Java, Javascript) command + / Ctrl + /
コメントアウト(xml, html, css) command + Shift + C Ctrl + Shift + C
クイックアクセス command + 3 Ctrl + 3
インデントの訂正 command + 1 Ctrl + 1
インポートの編成 shift + command + O Ctrl + Shift + O
1行下へ移動 option + Down Alt + Down
宣言を開く F3 F3
型階層を開く F4 F4
名前を指定してファイルを開く command + Shift + R Ctrl + Shift + R
Javaのタイプ名を指定してファイルを開く command + Shift + T Ctrl + Shift + T
ヒストリーを戻る command + [ Alt + Left
ヒストリーを進む command + ] Alt + Right
新規ファイル command + N Ctrl + N
ファイルを閉じる command + W Ctrl + W
ファイルをすべて閉じる command + Shift + W Ctrl + Shift + W
次のビュー command + F7 Ctrl + F7
次のエディター command + F6 Ctrl + F6
次のパースペクティブ command + F8 Ctrl + F8
実行(RUN) shift + command + F11 Ctrl + F11
デバッグ(Debug) command + F11 F11
Maven Update Project option+ F5
TM Terminalを開く Crl + Alt + T Crl + Alt + T

Vrapper

Function Command
スクロール(Up) + j
スクロール(Down) + k
対応する括弧に移動 ] or [
左タブへ移動 gr
右タブへ移動 gt
次のメンバ(フィールド、メソッド) mm
前のメンバ(フィールド、メソッド) mp

tig

Function Command
add ファイルごと u
commit stasu viewでC
コミット毎の差分 main viewでd
ステータス main viewでS
ブランチをみる main viewでH
ヘルプをみる main viewでh

Presentation 素材

FLICKR

PAKUTASO

ASHINARI

KAGE-DESIGN

GRATISOGRAPHY

UNSPLASH

PoEAA 0.Introduction

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

Architecture

アーキテクチャとは

  • システムの主要な部品と、その間の相互関係
  • 後になって変更することが難しいような決定事項 (「変更することが難しい」は主観的で変わり得る)

Enterprise Applications

Enterprise Applicationは以下の特徴をもつ

  • 永続的データを扱う
  • データ量が多い
  • データは並列にアクセスされる
  • 大量のユーザーインターフェース画面がある
  • 別のエンタープライズアプリケーションと連携する
  • (別のエンタープライズアプリケーションとの)データの概念的不協和が存在する
  • 複雑なビジネス非論理性を扱う

Kind of Enterprise Application

以下の3種類を代表例として考える

  • B2Cオンライン小売業(ロジックは簡潔だが、処理量大)
  • 賃貸契約の自動化システム(処理量小だが、ロジックが複雑)
  • 小規模企業の費用追跡システム(将来の拡張性が重要)

それぞれ異なる困難性を持ち、全てに適用可能な唯一のアーキテクチャというものはない

Thinking About Performance

  • アーキテクチャに関する決定の多くは性能に関わるものである
  • 原則としてパフォーマンス最適化は計測に基づくべきだが、性能に影響するアーキテクチャ上の決定は後半の最適化では修正が難しい場合がある
  • いくつかのガイドラインは提供するが、やっぱり計測に基づくべき
  • コンフィギュレーションが変わったら、やっぱり計測すべき
  • 設計上の決定が性能指標のすべてに同じように作用するとは限らない
  • エンタープライズシステムでは、キャパシティや効率性よりスケーラビリティが重要
  • 用語は以下のように定義する
用語 定義
応答時間 外部からの要求を処理するのにかかった時間
応答性 要求にたいしてどの程度すばやく受付応答するか
レイテンシ なんらかの応答を得るまでの最小時間
スループット 一定時間内処理量
ロード システムがどの程度負荷がかかった状態にあるか
ロードセンシティビティ ロードによりどの程度応答時間が変化するか
効率性 性能 / リソース
キャパシティ 最大の有効スループットまたはロード
スケーラビリティ リソースの追加が性能に与える影響
垂直スケーラビリティ または スケールアップ 1つのサーバーに能力を追加すること
水平スケーラビリティ または スケールアウト サーバーを追加すること

Patterns

  • パターンは実践にもとづいている
  • パターンのすべてを詳細に理解する必要はない
  • パターンは問題にそのまま適用できるものではない
  • いくつかのパターンは独立しているがいくつかは関連している

The Structure of the Pattern

  • 名前
  • 目的: 短い要約
  • 図: 視覚表現(大体UML図)
  • 動機となる問題: 複数の問題が動機となり得るが、著者が強い動機となる問題を記載)
  • How it works: 解決法を記載(特定のプラットフォームに依存する内容はインデントして記載)
  • When to Use: いつ使うべきか。他のパターントントレードオフも記載。
  • Future Reading: パターンに関する議論への参照
  • 例: JavaC#

Limitations of These Pattern

  • エンタープライズアプリケーション開発の包括的なガイドではない
  • パターンの使用は出発点であって終着点ではない

Markdown Sample

見出し1

見出し2

見出し3

見出し4

見出し5
# 見出し1
## 見出し2
### 見出し3
#### 見出し4
##### 見出し5

  • 箇条書きリスト
    • 箇条書きリスト(字下げ階層化)
    • 箇条書きリスト(字下げ階層化)
  • 箇条書きリスト
- 箇条書きリスト
    - 箇条書きリスト(字下げ階層化)
    - 箇条書きリスト(字下げ階層化)
- 箇条書きリスト

  1. a
  2. b
  3. c
9. a
9. b
9. c

none center right
a b c
dd ee ff
|none|center|right|
|---|---|---|
|a|b|c|
|dd|ee|ff|

id:leokite

id:leokite

id:leokite
`id:leokite`

引用記法 なお、引用記法内でMarkdown記法も使える。

  • リスト
  • リスト

    入れ子引用

>引用記法
> なお、引用記法内でMarkdown記法も使える。
>
> - リスト
> - リスト
>> 入れ子引用

require 'redcarpet'
markdown = Redcarpet.new("Hello World!")
puts markdown.to_html
```ruby
require 'redcarpet'
markdown = Redcarpet.new("Hello World!")
puts markdown.to_html
```

斜体

太字

太字の斜体

*斜体*
**太字**
***太字の斜体***




---
***
___

検索エンジンGoogleを使う。

検索エンジン[Google](http://www.google.com)を使う。

- [リンクその1][1]
- [長いURL][longURL]

[1]: http://www.leokite.hatenablog.com
[longURL]: http://ja.wikipedia.org/wiki/%E3%81%AF%E3%81%A6%E3%81%AA%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E4%B8%80%E8%A6%A7 "はてなのサービス一覧 - Wikipedia"

formula

![formula][matrix]
[matrix]:http://www.codecogs.com/eq.latex?%20%5Cbegin{pmatrix}a_{11}%20&%20a_{12}%20%5C%5C%20a_{21}%20&%20a_{22}%20%5Cend{pmatrix}

formula

![formula][root]
[root]:http://www.codecogs.com/eq.latex?\sqrt{variance}