LoginSignup
39
26

More than 5 years have passed since last update.

新しいモックライブラリ testdouble.js を使って js のモックテストを書く

Last updated at Posted at 2016-12-01

この記事は CureApp Advent Calendar 2016 2日目の記事です。

今日は新しいモックライブラリの testdouble.js の話です。

:performing_arts: モックとは

ソフトウェア開発におけるモックとは、テスト環境において本物を使ってしまうと何らかの理由で都合が悪いものを、テスト環境で本物とすり替えて使う何らかの 偽物 のことです。

「テスト環境において本物を使ってしまうと都合が悪いもの」の例としては、例えば、データベース接続、外部API接続など、環境を準備することができない/面倒/コストが高いものなどがあります。モックを使うことで、そのような構築コストの高い環境設定をスキップして、効率よくテストを実行することができます。

また、モックを使う別な理由として、テストの実行速度の高速化があります。ファイルアクセスやデータベースアクセスは一般的に遅いため、そこをモック処理に置き換えることでテストの実行を高速化することができます。

また、ライブラリのデザインによっては、「特定の振る舞いを持ったインターフェースに対する作用そのもの」が仕様である場合があります (例えばストラテジーパターンを使う場合)。そのような場合は、特定の振る舞いを持ったインターフェスをもったモックに対して実装を作用させてその結果を検証するというテストが必要になります。

testdouble.js

最近出てきた新しいモックライブラリで、testdouble.js があります。testdouble (テストダブル) というのは、モック、スタブ、スパイ、フェイクなど、細かくニュアンスが違う偽物系のオブジェクトを総称した呼び方です。(これらのニュアンスの違いを詳しく知りたい方は wikipedia/テストダブル か、 Martin Fowler の Mocks Aren't Stubs あたりを参照してください)

以下、testdouble.js でのモックテストの書き方を紹介していきます。

:pencil: 基本的な例

まず、例として、ある操作をすると、axios の get メソッドが呼ばれて、与えられたユーザの github profile が取得されるという処理のテストを考えて見ます。

実装
const axios = require('axios')

/**
 * @param {string} name GitHub ユーザ名
 * @return {Promise} ユーザ情報の Promise
 */
exports.getGithubProfile = function (name) {
  return axios.get('https://api.github.com/users/' + name)
}

このとき、axios の get メソッドを testdouble.js でテストダブル(この場合スタブ) に置き換えてテストを書くと以下のようになります。(テストランナーは mocha です。)

テスト
describe('getGithubProfile', () => {
  it('与えられたユーザの github profile を取得する', done => {
    td.replace(axios, 'get')

    td.when(axios.get('https://api.github.com/users/mojombo'))
      .thenResolve({dummy: 'value'})

    getGithubProfile('mojombo').then(data => {
      assert.deepEqual(data, {dummy: 'value'})

      td.reset()

      done()
    })
  })
})

td.replace(axios, 'get') という呼び出しで axios の get メソッドをテストダブルで置き換えています。

次に td.when(...).thenResolve({dummy: 'value'}) という呼び出しで、'https://api.github.com/users/mojombo' というパラメータで axios.get が呼ばれた場合に限り、{dummy: 'value'} というオブジェクトで resolve する Promise を返すように設定しています。

そして、テスト対象メソッドを呼び出した後、assert.deepEqual(data, {dummy: 'value'}) というアサーションで Promise の解決値を検証しています。呼び出しパラメータが上で設定した値とズレていれば、axios.get(...) で Promise が返らず、.then の呼び出しが失敗し、テストケースが失敗します。つまり、 axios.get を正しいパラメータで呼んでいること のテストになっています。

また、axios.get(...) の Promise の return を忘れている場合も、.then で例外になるため、 Promise をきちんと返していること のテストにもなっています。

最後に td.reset() でモックをリセットしています。モックをリセットすることで、axios.get が 本物に戻る ため、後ろのテストケースに影響が出ることを防いでいます。

上のように書くことで、github api へのアクセスができない環境でもテストを通すことができます。また、実際のネットワークアクセスがなくなるので、テストが高速に完了します。

td.verify

次に、例えば、与えられた「残金」と「商品価格」を受け取って残金が足りなければ、window.alert を呼ぶという処理を考えて見ましょう。

実装
/**
 * @param {number} money 残金
 * @param {number} price (買おうとしている)商品の価格
 */
exports.checkMoney = function (money, price) {
  if (money < price) {
    window.alert('残金が足りません')
  }
}

この場合、window.alert は純粋に副作用だけを期待して使っています。こういう場合は td.verify を使ってモックに対してどういうインタラクションがあったかをチェックする手法が有効です。

テスト
describe('checkMoney', () => {
  beforeEach(() => {
    td.replace(window, 'alert')
  })

  afterEach(() => {
    td.reset()
  })

  it('残金が足りない場合は警告ダイアログを表示', () => {
    checkMoney(500, 1000)

    td.verify(window.alert('残金が足りません'))
  })

  it('残金が十分ある場合は警告ダイアログを表示しない', () => {
    checkMoney(1500, 1000)

    assert.throws(() => td.verify(window.alert('残金が足りません')))
  })
})

説明していくと、 td.replace(window, 'alert') は前の例と同じで、window オブジェクトの alert メソッドをテストダブルで置き換えています。

次に一つ目のテストケースでは、まず、checkMoney(500, 1000) でテスト対象の処理を実行しています。

この場合、残金が商品価格より少ないため、アラートの方に処理が流れるはずです。そのことを次の行の td.verify(window.alert('残金が足りません')) という表記で確認しています。

td.verify(window.alert('残金が足りません'))

この、表現の意味は、window.alert('残金が足りません') という関数呼び出しがあったことを確認する、という意味です。その呼び出しがされていれば、式がパスしますが、呼び出しが無かった場合は、例外が投げられて、テストが失敗します。つまり、 window.alert('残金が足りません') という関数呼び出しがあったこと のテストになっています。

そして、最後に afterEachtd.reset() でモックを元に戻しています。

2つ目のテストケース (残金が十分ある場合は警告ダイアログを表示しない) はほとんど同じで金額が逆転しています。なので、window.alert('残金が足りません') という関数コールはされていないはずなので、そのことを assert.throws(...) で確認しています。(関数コールがされていないため、td.verify(...) はエラーになるため、その例外を拾って assert.throws のチェックがパスします。)

このようなテストを実装することで、window.alert という 副作用を期待した呼び出しの正しさ をテストで確認することができます。

以上が testdouble.js の基本的な使い方です。(より詳しい使い方は 公式ドキュメント を参照してください。)

sinon

さて、ここまで testdouble.js の使い方を紹介しましたが、js でモックライブラリというと sinon がほぼデファクトと認識されていると思います。最近書かれた JS の最新スタック速習記事のゼロから始めるJavaScript生活 でも、モックライブラリとしては sinon だけが挙げられています。

sinon.stub の例

例えば、sinon を使って、上のテスト (github API の例) を書くと下のようになります。

sinonで書いたテスト
describe('getGithubProfile', () => {
  it('与えられたユーザの github profile を取得する', done => {
    sinon.stub(axios, 'get')
      .withArgs('https://api.github.com/users/mojombo')
      .returns(Promise.resolve({dummy: 'value'}))

    getGithubProfile('mojombo').then(data => {
      assert.deepEqual(data, {dummy: 'value'})

      axios.get.restore()

      done()
    })
  })
})

簡単に説明すると sinon.stub(...).withArgs(...).returns(...) で axios.get をスタブに置き換えています。

その状態で、テスト対象の処理 (getGithubProfile('mojombo')) を実行し、Promise の解決値が正しいかどうかを検証しています。

最後に、axios.get.restore() でスタブ化していた axios.get を本物に戻しています。

sinon.mock の例

次に、checkMoney の例を sinon で実装してみると次のようになります。

sinonで書いたテストその2
describe('checkMoney @sinon', () => {
  it('残金が足りない場合は警告ダイアログを表示', () => {
    const mock = sinon.mock(window).expects('alert').withArgs('残金が足りません')

    checkMoney(500, 1000)

    mock.verify()

    window.alert.restore()
  })

  it('残金が十分ある場合は警告ダイアログを表示しない', () => {
    const mock = sinon.mock(window).expects('alert').withArgs('残金が足りません').never()

    checkMoney(1500, 1000)

    mock.verify()

    window.alert.restore()
  })
})

sinon.mock(window).expects('alert').withArgs('残金が足りません') この呼び出しで、2つのことをしています。まず window.alert をモックで置き換えています。また同時に alert に対して、'残金が足りません' というパラメータで呼ばれるというエクスペクテーション(期待値)を設定しています。

次に checkMoney を実行して、次に、mock.verify() で上で設定したエクスペクテーションが妥当であったかどうかを確認しています。エクスペクテーションで期待したコールを alert が受けていれば mock.verify() がパスします。(期待を満たしたコールが無ければ例外が上がります)

最後に window.alert.restore() を実行して、モックを元に戻しています。

:warning: sinon の問題

上の例で見るとコードの量としてはそれほど大差はありません。ただし、いくつかの点で sinon には問題があると感じます。

型安全性

一つは型安全でないという点があります。例えば、window.alert.restore() というメソッドは便利ではあるものの、本来の alert には無いプロパティが追加されている形なので、型安全性が崩れています。また、上で紹介できませんでしたが、sinon は spy という API も持っており、

sinon.spy(axios, 'get')

の、ような呼び出しで、axios.get をスパイメソッドに置き換えることができますが、この API も axios.get.getCall(0) のような API でスパイした呼び出しを取り出すというデザインをしており、これも型安全性を破っています。

型安全で無いことの単純なデメリットとして、例えば、typescript, flow などで型チェックするとエラーになります。また、単にツールでチェックできないだけでなく、型をきちんとモックしていないということは、それだけ恣意性の高いデザインということになるため、忘れやすい / 覚えにくデザインであると言えます。(実際自分は sinon を覚えるのに相当苦労した上、未だに覚えられている感覚がありません。)

それに対して、testdouble.js はスタブするときは

td.when(axios.get('https://...'))

呼び出しを検証するときは、

td.verify(axios.get('https://...'))

などとなって、 モックに対しての呼び出し方が元のメソッドに存在する呼び出し方といつも同じ になるため、 自然で覚えやすいと感じられます。

API の複雑さ

sinon の API は非常にテクニカルです。スタブ / モック / スパイ / フェイクなどの概念の違いの理解を使い手に強要しています。それに対して、testdouble.js は 偽物 に相当するものは基本的には「テストダブル」1種類しかなく、それを 振る舞いの偽装 にも 呼び出しの検証 にも両方使うことが出来ます。つまり API が統一的にまとまっていて、覚えることが少ないです。

まとめ

今日は testdouble.js の紹介して、その特徴の一部について sinon との比較をしました。

次にモックライブラリを使う必要が生じた際に、もし sinon の API がイマイチ思い出せない / 覚えにくいと感じることがあれば是非 testdouble.js を試してみてはいかがでしょうか?

Happy mocking!

明日は、@sakymark さんの typescript の話です。

:octocat: Example

上で紹介したサンプルは下のリポジトリにもおいてあります。 npm install; npm test でテストが走る様子を確認できます。(要 node>=6.x)

tl;dr

  • 自然で可読性の高いモックテストが書ける testdouble.js オススメです! :smiley:
39
26
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
39
26