アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

TruffleのtestでEventがWatchingできなくて困った

今回はContractのEventをテストする時の注意点や思いついたtipsなどをまとめました。
結局は非同期で動いていることを意識してなかったのが原因ではあるのですが、node動かしながら試してたことをそのままテストに書いてもうまくいかないといういい例の一つにはなるのかなぁとか思っています。

Testでwatchが思った通りに動いてくれない!

まずこんなContractがあって

pragma solidity ^0.4.18;

contract ClientReceipt {
  event Deposit(address _from, bytes32 _id, uint _value);

  function deposit(bytes32 _id) public payable {
    // Any call to this function (even deeply nested) can
    // be detected from the JavaScript API by filtering
    // for `Deposit` to be called.
    Deposit(msg.sender, _id, msg.value);
  }
}

こんなテストを書いたのですがこれは失敗します。

const ClientReceipt = artifacts.require('ClientReceipt.sol')
contract('ClientReceiptTest', accounts => {
  let eventCount
  beforeEach(async () => {
    eventCount = 0
  })
  it.only("check event count.", async () => {
    const instance = await ClientReceipt.new()
    const event = instance.Deposit({}, {fromBlock: 0, toBlock: 'latest'})
    event.watch((err, log) => {
      if (!err) eventCount += 1
    })

    await instance.deposit(1)
    await instance.deposit(2)
    await instance.deposit(3)

    event.stopWatching()
    assert.isAtMost(3, eventCount)
  })
});



なぜテストが失敗するのか

冒頭でも書いてますが、watchが非同期で実行されるためです。watchにcallback関数を渡すとこのcallbackはnodeからのresponseを受け取ったタイミングで動作します。
そのため、このテストではwatchのcallbackが呼ばれる前にassert.isAtMost(3, eventCount)*1が実行されてしまうために失敗してしまいます。
ちなみにawait event.watch(callback)なんてしても無意味です。event.watch(callback)はfilterのpolling処理に対してcallbackを登録することしかしてないためです。


解決策

一番単純な方法はwaitをかけてあげればいいです。こんな感じに書き換えたら思った通りの動作をしてくれます。

const util = require("ethereumjs-util")
const to32Hex = (num) => util.bufferToHex(util.setLengthLeft(num, 32))
const ClientReceipt = artifacts.require('ClientReceipt.sol')
contract('ClientReceiptTest', accounts => {
  const delay = time => new Promise(res => setTimeout(() => res(), time))
  let eventCount
  beforeEach(async () => {
    eventCount = 0
  })
  it("check event count.", async () => {
    const instance = await ClientReceipt.new()
    const event = instance.Deposit({}, {fromBlock: 0, toBlock: 'latest'})
    event.watch((err, log) => {
      if (!err) eventCount += 1
    })

    await instance.deposit(to32Hex(1))
    await instance.deposit(to32Hex(2))
    await instance.deposit(to32Hex(3))

    await delay(2000)
    event.stopWatching()
    assert.isAtMost(3, eventCount)
  })
});

event.stopWatching()を呼び出す前に2秒ほど処理を停止して、callbackが呼ばれるまでの十分な時間待っています。


その他の解決策とかTips

そもそもtestでfilter.watchを使うのは不便な事が多いです。なので他の方法を考えてみます。

1.transactionReceiptから判断する

前回の記事でも書いた通り、EventはTransactionReceiptのlogsパートに記録されています。そこで、contract#methodのreceiptを受け取ってその中から該当のEventがあるかどうかで判断する方法です。

  it("check event count from receipt.", async () => {
    const count = (receipt) => {
      eventCount += receipt.logs.filter((e, i, a) => e.event == "Deposit").length
    }
    const instance = await ClientReceipt.new()

    count(await instance.deposit(to32Hex(1)))
    count(await instance.deposit(to32Hex(2)))
    count(await instance.deposit(to32Hex(3)))

    assert.equal(3, eventCount)
  })

# Truffleでreceiptを受け取るとlogsの中身を勝手にformaterで整形した結果を末尾に付け加えてくれるので判定が楽ですね。

2.event.get()でまとめて取得してから判別する方法

これが一番簡単だし応用も色々ききそうなきがします。個別にeventの中の値をみたい場合でも、そのタイミングでevent.get()を呼び出せば必要なeventだけ取得できますし、fromBlockをいじる方法でもいけそうですね。

  it("check event count by event.get().", async () => {
    const instance = await ClientReceipt.new()
    const event = instance.Deposit({}, {fromBlock: 0, toBlock: 'latest'})
    await instance.deposit(to32Hex(1))
    await instance.deposit(to32Hex(2))
    await instance.deposit(to32Hex(3))

    assert.equal(3, event.get().length)
  })



Eventのデータを確認する場合

こう、formatterとか作らないとかなーとか思ってたのですが、そもそもとしてevent.getで帰ってくるデータがすでに整形済みでargsの中にEventのパラメータが格納されています。なので簡単にEventの中身をチェックする事ができます。
例えば、発行されたEventの_idを検証したい場合はこう書けばOKです。

  it("check event ids.", async () => {
    const instance = await ClientReceipt.new()
    const event = instance.Deposit({}, {fromBlock: 0, toBlock: 'latest'})
    await instance.deposit(to32Hex(1))
    await instance.deposit(to32Hex(2))
    await instance.deposit(to32Hex(3))

    assert.equal(3, event.get().length)
    assert.deepEqual([1, 2, 3], event.get().map((e) => Number(e.args._id)))
  })

*1:assert.equalじゃなくてassertisAtMostにしてるのはたまに同じeventが2回callbackされる時があるためです。