testdouble.jsによるローカル環境でのスタブ作成

# はじめに

クライアントサイドで実行されるJavaScriptの開発中、未完成なモジュールのメソッドやAPIリクエストを行う処理があると、仮に動作させるためのスタブが必要になる。
リモートサーバーを用意してAPIスタブを作ったりしても良いが、テストライブラリの[testdouble.js](https://github.com/testdouble/testdouble.js)を使えば、ローカル環境上で動くスタブを簡単かつ柔軟に作成できる。

# インストール

```
npm install -D testdouble
```

次にスタブ設定用のファイルを用意する。

```js:stub.js
var td = require('testdouble');
```

クライアントサイドで実行する処理をスタブ化するため、webpackなどのモジュールバンドラーを用いて、開発用ビルドではこのファイルもバンドルする。
本番用のビルドからは省く必要があるので、エントリポイントとなるファイルには、ビルド環境に応じてスタブを含めるかの分岐を記述しておく。

```js
if(NODE_ENV !== 'production') {
  require('./stub.js'); // testdouble.jsのスタブ設定用ファイル
}
```

参考：[webpackで環境変数をモジュールに渡す](https://qiita.com/hal9188/items/ae4302412c179a1034cb)

# 使い方

## メソッド

`td.replace`を用いると、指定したメソッドを置き換えられる。

```js

var text = {
  show: function(str) { console.log(str); }
};

text.show('test'); // test

td.replace(text, 'show', (str) => { console.log(`show:${str}`) });

text.show('test'); // show:test
```

また、メソッドに渡す引数に応じて返り値を変えたい場合は、`td.when`を使う。

```js
var browser = {
  isIE: function(ua) { return /Trident|MSIE/.test(ua); }
};

browser.isIE('Trident'); // true
browser.isIE('MSIE'); // true

td.replace(browser, 'isIE');
td.when(browser.isIE('Trident')).thenReturn(true);
td.when(browser.isIE('MSIE')).thenReturn(false);

browser.isIE('Trident'); // true
browser.isIE('MSIE'); // false
```

## API

testdouble.jsには擬似XHRの機能はないが、`td.replace`でAPIリクエストを投げるメソッドを置き換えることで、擬似的にAPIのスタブを実現できる。

```js
var ajax = {
  get: function(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.onreadystatechange = function(e) {
      if(this.readyState === 4) {
        if(this.status === 200) {
          var res = JSON.parse(this.responseText);
          callback(res);
        }
      }
    }
    xhr.send(null);
  }
};

td.replace(ajax, 'get');
td.when(ajax.get('http://api.example.com/get')).thenCallback(JSON.parse('{"status":"success"}'));

ajax.get('http://api.example.com/get', function(res) {
  console.log(res); // {"status":"success"}
});
```

また、`td.when`に`delay`オプションを設定すると、非同期通信のレスポンス待ちが再現できるので、待ち時間中の画面状態も確認できる。

```js
td.when(hoge.get('http://api.example.com/get'), { delay: 500 }).thenCallback(JSON.parse('{"status":"success"}'));
```

## 引数

渡す引数に応じて挙動を変えたい場合は、`td.matchers`を用いる。

### 何でも

`td.matchers.anything()`ではどんな引数でもOKとなる。ただし引数の数自体は厳密に渡す必要がある。

```js
var hoge = td.function();

td.when(hoge(td.matchers.anything())).thenReturn('fuga');

hoge('test'); // fuga
hoge(true); // fuga
hoge(9); // fuga
hoge(); // undefined
hoge('test', true); // undefined
```

### 型指定

`td.matchers.isA()`では`Number`や`String`といった引数の型を指定できる。

```js
var hoge = td.function();

td.when(hoge(td.matchers.isA(Number))).thenReturn('fuga');

hoge(9); // fuga
hoge('nine'); // undefined
```

### 〜を含む

`td.matchers.contains()`では特定の文字列を含むかの指定ができる。

```js
var hoge = td.function();

td.when(hoge(td.matchers.contains('nine'))).thenReturn('fuga');

hoge('nine'); // fuga
hoge('none'); // undefined
```

また、正規表現も使える。

```js
var hoge = td.function();

td.when(hoge(td.matchers.contains(/ni/))).thenReturn('fuga');

hoge('nine'); // fuga
hoge('none'); // undefined
```

この他にも`td.matchers.contains()`は配列やオブジェクトの一致判定にも使える。それらの使い方は[公式ドキュメント](https://github.com/testdouble/testdouble.js/blob/master/docs/5-stubbing-results.md#tdmatcherscontains)を参照。

# 終わりに

ローカル環境でスタブを構築できることにより外部への依存が少なくなるため、開発中のモジュールやスタブ用のAPIの仕様変更によって突然動かなくなるといった状況を防ぎ、開発効率を上げることができる。
一方で、外部の仕様が変更されても関係なく動作してしまうため、合わせてスタブも更新するのを忘れずに。