jest入門 - Mockテストと非同期テスト



## Jestとは

Facebookで開発しているUnitTestフレームワーク。

https://facebook.github.io/jest/

特徴は以下です。

- FAMILIAR APPROACH
  - Jasmineベースなので記法は馴染みやすいものです。
- MOCK BY DEFAULT
  - CommonJSスタイルの`require()`をMockすることで単体テストを実現します。
- SHORT FEEDBACK LOOP
  - サクッとテストできる。
  - ブラウザとかPhantom.jsとか要らないよって意味だと思う。。。

## 何がいいのか？

素のJasmineと比べてこんな特徴があるようです。

- テストファイルを勝手に探してくれる。
- DefaultでMockされるので、あえて記述しない限り純粋な単体テストになる。
- mockを使うことで、非同期処理を同期的に書ける。
  - Jasmine2.Xでは非同期サポートがされたとか聞きましたがよく知らない。
- jsdomを使うことで、HTMLを含んだテストもできる。
- 並列実行できるので高速
  - 他のツールを知らないけど、今のところ速いとは思わない。。

加えて個人的には、実コードの設計・フレームワークに関係なくテストできる、と思っています。

DocumentではAngularとの比較がありますが、Angularは引数にいくつか突っ込んでおくことで差し替え可能にしている。
これは、実際の動作時にはフレームワークが裏からよろしくDIしていることで実現されている。

Jestはそんなことを考えないで、require/importなどのモジュールスタイルで書かれていれば、フレームワークや設計に依存しないで単体テストを書くことができる。逆に言えば実コードを書くときにそのへん気にしなくていい。

## テスト内容

今回は、

- 普通の関数のテスト
- 外部モジュールを含むfunctionのテスト
- 非同期を含むfunctionのテスト

を動かしてみます。

## 構成

```
$ tree .
.
├── gulpfile.js
├── package.json
└── src
    ├── index.html
    ├── main.js
    ├── PromiseSample.js
    ├── __tests__
    │   └── PromiseSample-test.js
    └── util.js
```

コマンドラインや`npm run`ではなく、gulpのタスクとして実行します。
理由は、後でテストの粒度を調整できるからです。
例えば、全体をテストする用と自分の開発中のフォルダだけテストする用と、みたいな。

一応ブラウザでの挙動も見たいので、webpackでまとめるようにしています。

### package.json

```json:package.json
{
  "dependencies": {
    "bluebird": "~2.6.4"
  },
  "devDependencies": {
    "6to5-jest": "^1.0.0",
    "6to5-loader": "^2.0.0",
    "gulp": "^3.8.10",
    "gulp-webpack": "^1.1.2",
    "harmonize": "^1.33.7",
    "jest-cli": "^0.2.1",
    "rimraf": "^2.2.8"
  }
}

```

今回はES6スタイルでモジュールを取り扱うので、ES5への変換を噛ませている。

6to5はtranspilersの中でもjsxにも対応してたりして良いとkoba04さんやazuさんが言っているのでこれ一択で。

http://blog.koba04.com/post/2015/01/13/a-combination-of-reactjs-and-6to5/

`6to5-jest`はjestのPreprocessor用、`6to5-loader`はwebpack用。

### gulpfile.js

```js:gulpfile.js
var gulp = require('gulp');
var webpack = require('gulp-webpack');
require("harmonize")();

gulp.task('cleanBuild', function (cb) {
  var rimraf = require('rimraf');
  rimraf('./build/', cb);
});

gulp.task('copyIndex', ['cleanBuild'], function () {
  return gulp.src('./src/index.html')
  .pipe(gulp.dest('./build/'));
});

gulp.task('build', ['copyIndex'], function (cb) {
  var config = {
    entry: './src/main.js',
    output: {
      filename: './build/bundle.js'
    },
    devtool: 'inline-source-map',
    module: {
      loaders: [
    { test: /\.js$/, loader: '6to5-loader' }
      ]
    },
    resolve: {
      extensions: ['', '.js']
    }
  };

  return gulp.src('')
  .pipe(webpack(config))
  .pipe(gulp.dest(''));
});

gulp.task('jest', function (callback) {
  var jest = require('jest-cli');
  var options = {
    rootDir: __dirname,
    scriptPreprocessor: "<rootDir>/node_modules/6to5-jest",
    unmockedModulePathPatterns: [
      "<rootDir>/node_modules/bluebird"
    ],
    testDirectoryName: "__tests__",
    testFileExtensions: ["js"]
  }

  var onComplete = function (result) {
    callback();
  }
  jest.runCLI({config: options}, __dirname, onComplete);
});

```

jestやwebpackの設定もここにぶち込んでいます。

- `gulp build`でwebpackがよろしくまとめてくれます。
- `gulp jest`でjestのテストが走ります。

jestタスクの中の、`unmockedModulePathPatterns`にbluebirdが入ってるのが地味に重要です。PromiseはMockされては困るので。

（harmonizeは後述します。）

### src

```html:index.html
<!DOCTYPE html>
<html>
<body>
</body>
<script src="./bundle.js"></script>
</html>
```

もはやhtmlは邪魔なレベル。

```js:main.js
import PromiseSample    from './PromiseSample';

PromiseSample.proc().then(function(str){alert(str);});
```

エントリポイント。

```js:PromiseSample.js
'use strict';
import util   from './util';

export default {
  proc: function () {
    var str = this.preFunc();
    return util.getPromise(str)
    .then(this.postFunc);
  },
  preFunc: function () {
    return "pre";
  },
  postFunc: function (str) {
    return str + " : done";
  },
};
```

util.getPromiseがpromiseオブジェクトを返してくれるので、
それを含んだ処理が書いてあったりします。

```js:util.js
'use strict';
import Promise   from 'bluebird';

export default {
  getPromise: function(str){
    return new Promise(function(resolve, reject) {
      setTimeout(function(){
        return resolve(str);
      }, 1000);
    });
  }
};
```

一秒経つとPromiseがresolveするだけです。
実際にはajaxを扱ったりすると思います。

### __tests__

```js:PromiseSample-test.js
'use strict';

jest.dontMock('../PromiseSample');

import promiseSample              from '../PromiseSample';
import util  from '../util';
import Promise                from 'bluebird';

describe('promise test', function() {
  it('pre test', function() {
    var result = promiseSample.preFunc();
    expect(result).toBe('pre');
  });

  it('proc error test', function() {
    expect(promiseSample.proc).toThrow();
  });

  it('proc through test', function() {
    util.getPromise.mockReturnValue(Promise.resolve('ss'));
    var result = promiseSample.proc();
    console.log('result is promise object');
    console.log(result);
  });

  pit('proc return value test', function() {
    util.getPromise.mockReturnValue(Promise.resolve('ss'));
    return promiseSample.proc().then(function(value) {
      expect(value).toBe('ss : done');
    });
  });
});
```

では、テストの中身を見ていきます。

#### 事前準備

```js
jest.dontMock('../PromiseSample');
```
でmockにしない（実物を扱う）モジュールを指定します。基本的にはテスト対象のみだと思います。

```js
import promiseSample              from '../PromiseSample';
import util  from '../util';
import Promise                from 'bluebird';
```

ここで各種モジュールをimportするのですが、promiseSampleはdontMockなので実体、Promiseもbluebirdは設定で外してるので実体です。utilだけMockが差し込まれています。

#### 普通の関数のテスト

```js
it('pre test', function() {
    var result = promiseSample.preFunc();
    expect(result).toBe('pre');
  });
```

普通のテストです。Jasmineベースなので同じ記法らしいです。

#### 外部モジュールを含むfunctionのテスト

```js
it('proc error test', function() {
    expect(promiseSample.proc).toThrow();
  });
```

promiseSample.procの処理のなかにutilが含まれているのですが、utilはMockされていて正しく動かないのでエラーになります。

そこで、こうしてあげます！

```js
it('proc through test', function() {
    util.getPromise.mockReturnValue(Promise.resolve('ss'));
    var result = promiseSample.proc();
    console.log('result is promise object');
    console.log(result);
  });
```

ここがJestのMockの真骨頂です！

utilはMock化されていてそのままでは動かないので、`util.getPromise`の動作を定義してあげる必要があります。今回の例では、`util.getPromise()`とすると、`Promise.resolve('ss')`が返ってくる、ということにしました。
なんと、この動作を、promiseSampleの中にあるutilもしてくれるようになるのです。

当然、最後まで処理が流れるのでテストはパスします。consoleを見ると、Promiseっぽいオブジェクトがちゃんと返ってきています。

#### 非同期を含むfunctionのテスト

```js
pit('proc return value test', function() {
    util.getPromise.mockReturnValue(Promise.resolve('ss'));
    return promiseSample.proc().then(function(value) {
      expect(value).toBe('ss : done');
    });
  });
```

jasmine-pitというjasmineで非同期テストをできるHelperを使うことでテストできます。

先ほどと同じようにMock関数に返り値を定義して、procを()を実行すると最後まで処理が進みます。その結果をthenで受け取ることで、テストができるようになるのです。

以上で、冒頭に上げた三種類のテストができました。


### ハマったところ

#### harmony flag

gulpでjestを走らせるときに、普通にやろうとしたら

```
Error: Please run node with the --harmony flag!
```

と怒られました。node詳しくなくて原因不明なので困りました。（jest.runCLIがharmonyオプション必須なのでしょうか。）

とりあえずharmonizeというのを実行するとオプションを有効にしてくれるそうなので、それで対応してます。どうせES6記法で書いてるので問題ないはず。たぶん。

#### commonJSスタイルではMockされなかった

なぜか、自分で作ったモジュールをcommonJSスタイルで読み込むとmockされなかったんですよね。たぶん拡張子がどうとか相対パスとかで、別のモジュールか何かと認識されたのではと思ってますが、原因は不明です。

前述の通りkoba04さんらが6to5使ってると聞いて、webpackのloaderと統一したかったのもあってこちらに移行しました。

## 懸念点

### 実行速度がけっこう遅い。

デフォルトでは「__tests__」フォルダ以下を全て探してテストしてくれて便利でいいのですが、1ファイル2秒くらいはかかりそうなので数十ファイルあったら1分とかかかります。
「SHORT FEEDBACK LOOP」になってない気がする。。。

回避策としては、jestのテスト対象を分割しておくことです。
config.testPathDirsあたりをいじれば、テスト対象を調整できます。

### ドキュメントがしょぼい

サンプルも少ないし、APIは変数名以上の情報がないｗ

Reactはすごい丁寧だったのに。。。

今回もreturnValueみたいなのの動きを確かめるまでにすごい時間がかかった。

## まとめ

Jestいい感じ。（他のテストツール触ったことない。）