Node.jsでmochaやproxyquireを使った単体テストをやってみたメモ

Node.jsで単体テストを作ってみたことがなかったので、標準のアサーションモジュールの[assert](https://nodejs.org/api/assert.html)とテスティングフレームワークの[mocha](https://mochajs.org/)を使ってみた時のメモ。また、[proxyquire](https://github.com/thlorenz/proxyquire)を使ってAWS-SDKのスタブ化も行ってみました。

# バージョン

+ Node.js->4.2.2
+ mocha->2.3.4
+ proxyquire->1.7.3

# 参考

+ [Node.js assert](https://nodejs.org/api/assert.html)
+ [mocha](https://mochajs.org/)
+ [hokaccha.hamalog v2 テストフレームワーク mocha](http://d.hatena.ne.jp/hokaccha/20111202/1322840375)
+ [[Node.js] AWS lambdaでaws-sdkをスタブ化してテストする](http://qiita.com/szk3/items/39eeac58b2a0a2078375)



# assertだけでテストを作成してみる

mochaのようなテストティングフレームワークを使わなくてもNode.js標準モジュールのassertを使うだけでもテストは可能です。

今回は以下のようなモジュールのテストを行ってみます。

```lang:movie.js

var Movie = function(title) {
  this.setTitle(title);
}

Movie.prototype.getTitle = function() {
  return this.title;
}

Movie.prototype.setTitle = function(title) {
  this.title = title;
}

module.exports = Movie;

```

テストコードを書いてみます。
assertモジュールを使ってアサーションをしている以外の部分は通常の実装と変わりはありません。


```lang:testMovie.js
var Movie = require('./movie');
var assert = require('assert');

function constTest() {
  var movieA = new Movie('Star Wars');
  assert.equal(movieA.getTitle(),'Star Wars');
}

function setterTest() {
  movieA.setTitle('Mad Max');
  assert.equal(movieA.getTitle(),'Mad Max');
}
```

以下のように実行し、特に何も出力されず終了すればテストは全て成功です。

```bash
$node testMovie.js
```

テストが失敗した場合、以下のようにAssertionErrorがスローされてエラーとなります。

```bash
node testMovie.js

assert.js:89
  throw new assert.AssertionError({
  ^
AssertionError: 'Mad Max' == 'Star Wars'
    at Object.<anonymous> (/Users/toshihirock/node/module/testMovie.js:11:8)
    at Module._compile (module.js:435:26)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:311:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:136:18)
    at node.js:963:3
```

上記のようにassertだけでもテストは可能ではあるのですが、テスト結果をまとめたり、特定のテストだけ実行することや、テスト失敗時の件数を数えるような機能などもありません。

上記に挙げたような機能についてはテスティングフレームワークを利用することで簡単に導入が可能となります。

# テスティングフレームワークのmochaを使ってみる

mochaはサードパーティのテスティングフレームワークなので、npmを使ってインストールします。

```bash
$npm install -g mocha
```

次にmocha用にtestディレクトリを作成します。
mochaではtestディレクトリ配下のコードをテストコードとみなして動作するためです。

```bash
$mkdir -p mocha-test/test
$cd mocha-test
$cp /path/to/movie.js .
$touch test/movie.js
```

では早速テストコードを書いていきます。mochaではテストの書き方のスタイルとしてBDDスタイル、TDDスタイル、Exportsスタイル、QUnitスタイルの4つのスタイルを提供していますが、今回の例ではBDDスタイルでの記述を行います。

```lang:test/movie.js
var Movie = require('../movie');
var assert = require('assert');

describe('movie', function() {
  it('コンストラクタで設定したタイトルが取得できること', function() {
    var movieA = new Movie('Star Wars');
    assert.equal(movieA.getTitle(), 'Star Wars');
  });

  it('セッターメソッドでタイトルが変更できること', function() {
    var movieA = new Movie('Star Wars');
    movieA.setTitle('Mad Max');
    assert.equal(movieA.getTitle(), 'Mad Max');
  });
});
```

ポイントは以下です。

+ describe()でテストをグループ化する
+ it()にテストの確認内容と実際のテストコードを記述する
+ テストコードの部分ではassetを使ってアサーションを行う(mochaはアサーションの機能は提供しないのでassertを利用）

テストの実行は`mocha`コマンドによって可能です。

```bash
$mocha


  movie
    ✓ コンストラクタで設定したタイトルが取得できること
    ✓ セッターメソッドでタイトルが設定できること


  2 passing (10ms)
```


assertだけの場合、成功しても何も出力されませんでしたが、mochaを使う事で成功したテストが何か、また実行したテスト数やテスト実行時間などの情報が表示されました。

出力はjsonやhtml形式での出力も可能です。詳細は公式ドキュメントに記述してあります。

# フックを使ってみる

[mocha Hooks](https://mochajs.org/#hooks)

例えば先ほどのテストではMovieクラスの生成については共通の処理になっており、テストケースが増えると毎回書くのが面倒です。また、テスト時に一時的にファイルを作成するなどの事前準備が必要なテストもあるかと思います。

mochaではグループ化したテストやテスト毎に事前処理や事後処理などを設定することができます。以下のメソッドを使う事で可能です。

+ before()->各describeの前に実行
+ after()->各describeの後に実行
+ beforeEach()->各it()の前に実行
+ afterEach()->各it()の後に実行

今回の例では各テストの前にmovieクラスの生成をやってみたいのでbeforeEachメソッドを使ってみます。

```lang:test/movie.js
var Movie = require('../movie');
var assert = require('assert');

describe('movie', function() {
  var movie;

  // 各テストの処理の前にMovieクラスを生成
  beforeEach(function() {
    movie = new Movie('Star Wars');
  });

  it('コンストラクタで設定したタイトルが取得できること', function() {
    assert.equal(movie.getTitle(), 'Star Wars');
  });

  it('セッターメソッドでタイトルが設定できること', function() {
    movie.setTitle('Mad Max');
    assert.equal(movie.getTitle(), 'Mad Max');
  });
});
```


# 非同期のテストをやってみる

[mocha asynchronous code](https://mochajs.org/#asynchronous-code)

Node.jsでは非同期の処理を実装することもあるかと思いますが、mochaでは非同期処理のテストもサポートしています。
done()という関数を使う事でテストが可能です。

今回は例として以下のようなfsモジュールを使った非同期処理のコードを作成します。(fsモジュールのexistsはdeprecaetになってますが。。。）

```lang:index.js
var fs = require('fs');

exports.exists = function(path, cb) {
  fs.access(path, fs.R_OK, function(err) {
    if (err) cb(err, false);
    else cb(null, true);
  });
}
```

上記に対応するテストコードを書いてみます。

```lang:test/index.js
var assert = require('assert');
var index = require('../index');
var fs = require('fs');

describe('非同期', function() {

  // テスト用一時ファイルの作成
  before(function(done) {
    fs.writeFileSync('./test/sample.txt', '');
    done();
  });

  // テスト用一時ファイルの削除
  after(function(done) {
    fs.unlinkSync('./test/sample.txt');
    done();
  });

  it('ファイルが存在する場合のテスト', function(done) {
    index.exists('./test/sample.txt', function(err, result) {
      assert.equal(err, null);
      assert.equal(result, true);
      done();
    });
  });

  it('ファイルが存在しない場合のテスト', function(done) {
    index.exists('./test/hoge.txt', function(err, result) {
      assert.notEqual(err, null);
      assert.equal(result, false);
      done();
    });
  });
});
```

非同期テストに関するポイントは以下です。

+ `it('hogeテスト', function(done)`という感じでit()の第2引数に指定する関数の引数にdoneを指定する
+ 非同期処理が終わった後に`done();`を呼び出すことでmochaにテストの終了を通知する

また、before,afterのフック処理を使ってテスト時に一時的に利用するファイルの作成、削除も併せて行っています。

# requireしているモジュールのスタブをしてみる

自分の場合、AWS-SDKを使って何かを作ることが多く、AWS-SDKの結果をスタブ化したいと考えることがありました。

上記のようなスタブ化については以下の記事がドンピシャでやりたい事が書いてありました。

[[Node.js] AWS lambdaでaws-sdkをスタブ化してテストする](http://qiita.com/szk3/items/39eeac58b2a0a2078375)


今回は上記の記事で紹介されている[proxyquire](https://github.com/thlorenz/proxyquire)を使ってAWS-SDKをスタブ化してみます。(AWS-SDKだけでなく、requireするモジュールであれば同じ対応が可能だと思います。）

例として以下のような内部でAWS-SDKのS3のlistBucketsメソッドを使っているものがあるとします。（以下のコード自体はAPIをラップしているだけでほぼ意味のないものですが。。）

```s3.js
var AWS = require('aws-sdk');

var s3 = new AWS.S3();

exports.listBuckets = function(cb) {
  s3.listBuckets(function(err, data) {
    if (err) cb(err, null);
    else  {
       // 何かしらの処理
      cb(null, data);
    }
  });
}
```

上記では`AWS.S3.listBuckets`の結果をスタブ化してバケットが一つの時、複数の時、何もない時などに変えてテストがしたいと考えたとします。
そんな時に便利なのが、proxyquireというモジュールになります。

具体的には以下のようにテストが書けます。

```test/s3.js
var assert = require("assert")
  , proxyquire = require('proxyquire')
  ;

describe("s3", function() {
  it("バケットが一つだけ取得できた場合のテスト", function(done) {

    // S3.listBucketsをした時に返却するスタブの内容
    var result =
      { Buckets:
          [ {Name: 'first-buckets', CreationDate: Date.now()}],
        Owner:
          { DisplayName: 'toshihirock', ID: 'hogefuga' }
      };

    // スタブの設定
    var proxys3 = function() {
      this.listBuckets = function(cb) {
        cb(null, result);
      };
    };
    var s3 = proxyquire('../s3', {'aws-sdk': {S3: proxys3} });

    // テスト
    s3.listBuckets(function(err, data) {
      assert.equal(err, null);
      assert.equal(data.Buckets[0].Name, 'first-buckets');
      done();
    });
  });

});
```

resultで予め返却させたい変数を設定しています。（どのような値が返却されるかはドキュメントや実際に値を取得し確認しておきます）
次にどの関数をスタブ化させたいかを記述し、最後は今まで通り、テストを記述すればOKです。

今回は試せなかったのですが、[Sion.js](http://sinonjs.org/)を使う事でスタブ、モック、スパイなどを使う事でできるようなので、別途また時間のあるタイミングで勉強したいと思います。

+ [jsでTDD！MochaとChaiとsinon.js入門](http://lxyuma.hatenablog.com/entry/2013/12/15/211637)
+ [javascriptのテストのはなし：Sinon.JS(その1)](http://dev.classmethod.jp/etc/javascript_testing_framework_sinonjs-1/)