この頃流行りのJestを導入して軽快にJSをテストしよう

## Jestとは
[Jest](https://facebook.github.io/jest/)はFacebook製のJSテストプラットフォームです。

近頃、急速にユーザーが増えているようで、ある調査では2017年にMochaやJasmineを超えてトップの使用率を獲得しているようです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/6158/433d11a4-2d05-5b45-9645-ef3fc20f92e1.png" width=500 />
出展: https://ashleynolan.co.uk/blog/frontend-tooling-survey-2018-results

最近、自分のプロジェクトでもJasmine + KarmaからJestへ移行し、以下のようなメリットを得られました。

- ブラウザの起動がないぶん軽快に動く
    - 実ブラウザ上ではなく、JSDOMのエミュレーション環境でテストが実行される
- レポートが見やすい
    - テスト実行進捗がプログレスバーで表示され、Failしたテストの閲覧もしやすい
- カバレッジを簡単に取得できる
    - 実行時にオプションをひとつ追加するだけ

この記事では、導入時にハマりどころがいくつかあったので、導入方法とともにハマったポイントと対処法を紹介します。

## 前提
以下バージョンを前提としています。

- Jest 23.1.0

## インストール
まず基本となる2つのパッケージをインストールします。

```bash
$ npm install --save-dev jest babel-jest
```

※ Babelを使っていない場合には `babel-jest` は不要ですが、多くのプロジェクトで使われていると思われるため、以下の説明ではBabelを使用していることを前提に書いています


TypeScriptファイルをテストしたい場合は、 加えて `ts-jest` をインストールしてください。

```bash
$ npm install --save-dev ts-jest
```

Vueファイルをテストしたい場合には、 加えて `vue-jest` をインストールしてください。

```bash
$ npm install --save-dev vue-jest
```

## 設定ファイル
jestの設定を定義します。

設定を定義する場所は以下の選択肢があります。

1. 独立したファイルに定義する
2. `package.json` に定義する

それぞれ、設定できる内容に違いはありませんので、どちらを選択しても問題ありません。

### 1. 独立したファイルに定義する場合の例
```javascript:jest.config.js
module.exports = {
  verbose: true
};
```

### 2. package.jsonに定義する場合の例
```javascript:package.json
{
  "name": "my-project",
  "jest": { // <-- jestプロパティ以下に設定を書く
    "verbose": true
  }
}
```
### 設定例
テストを始めるにあたって必要な設定を行います。

`jest.config.js`ファイルをプロジェクト直下に作成、もしくは、`package.json`ファイルに追記します。

```javascript:jest.config.js
module.exports = {
  transform: {
      '^.+\\.js$'  : '<rootDir>/node_modules/babel-jest',
      '.*\\.(ts)$' : '<rootDir>/node_modules/ts-jest',    // TypeScriptファイルをテストする場合
      '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'    // Vueファイルをテストする場合
  },
  moduleFileExtensions: ['js', 'ts', 'vue'] // テスト対象の拡張子を列挙する
}
```

上記のように定義することで、トランスパイルしたうえでテストを実行してくれるようになります。
このとき、 `babel-jest` は `.babelrc` 、 `ts-jest` は `.tsconfig.json` を参照しトランスパイルを行います。

### import/export構文を使えない問題の対処
多くのブラウザアプリケーションではESModuleの `import/export` 構文を使用しているかと思います。
しかし、JestはNode.js上でテストを実行するため、Node.jsのモジュールシステムのCommonJSである必要があります。

（現時点のNode.js <= v10ではフラグを立て、ファイル拡張子の変更を行わないとESModuleに対応させることができません）

たとえば、下のような `.babelrc` の場合、ESModuleはCommonJSに変換されないため、このままJestを実行するとエラーとなります。

```javascript:.babelrc
{
  "presets": [
    ["env", {
      "modules": false // <-- CommonJSに変換していない
    }]
  ]
}
```

そこで、**テスト実行時のみ** ESModuleをCommonJSに変換することで、Jestでも`import/export`構文を使用できるように対応します。

まず、babelのpluginをインストールします。

```bash
$ npm install --save-dev babel-plugin-transform-es2015-modules-commonjs
```

test環境でのみpluginを有効とするように `.babelrc` に以下のように設定を追加します。

```javascript:.babelrc
{
  "env": {
      "test": { // <-- NODE_ENV=testの場合のみpluginが有効になる
          "plugins": [
              "transform-es2015-modules-commonjs"
          ]
      }
  }
}
```

参考: https://github.com/facebook/jest/issues/2081

## テストファイル
以下の`sum.js`のテストを書いてみます。

```javascript:sum.js
function sum(a, b) {
  return a + b;
}

export default sum;
```

Jestは`*.test.js`もしくは`*.spec.js`をテストファイルとして検出しテストを実行します。
また、`__tests__`ディレクトリ以下のすべてのファイルをテストファイルとして扱います。

ここではファイル名を`sum.test.js`としました。

```javascript:sum.test.js
import sum from './sum';

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});
```

テストの記法については他に詳しく解説してくれている記事があるので、ここでは説明を省きます。

自分は下の記事を参考にしました。

- [Facebook製のJavaScriptテストツール「Jest」の逆引き使用例](https://qiita.com/chimame/items/e97883fd46b67529d59f#%E3%83%86%E3%82%B9%E3%83%88%E6%A7%8B%E6%96%87%E7%B7%A8)

Jasmineやmochaなど、別のJSテストフレームワークを使った経験がある方なら、Jestでもほとんど同じ書き方ができるため、すぐに書き始められると思います。

## テストの実行
npm-scriptsに、Jestを実行するコマンドを設定します。

```javascript:package.json
"scripts": {
  "test": "NODE_ENV=test jest"
},
```

コマンドラインより実行

```bash
$ npm test
```

以下のようなレポートの出力を確認できたら成功です :tada:

<img src="https://qiita-image-store.s3.amazonaws.com/0/6158/3548bc64-552b-d715-7451-07ce2771c36c.png" width=500>


## カバレッジの取得
カバレッジの取得は、`--coverage`オプションを加えるだけで実現できます。

```javascript:package.json
  "scripts": {
    "test": "NODE_ENV=test jest ./path/to/test_directory",
    "test-with-coverage": "npm test -- --coverage" // <-- カバレッジも取得する場合はこちらを実行
  },
```

コマンドラインより実行

```bash
$ npm run test-with-coverage
```

カバレッジのレポートが表示されます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/6158/baf6b4c5-f700-a3c3-2f2d-870801f6dbca.png" width=500>

また、`coverage/lcov-report/index.html`を開くと詳細なレポートを参照できます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/6158/485c44c6-7166-cb2f-d70c-4bab4963c778.png" width=700>

<img src="https://qiita-image-store.s3.amazonaws.com/0/6158/c406d492-2137-c896-ad2f-91f8c8c06a7a.png" width=700>

## ESLintの対応
ESLintを使っている場合、テストファイル中の`test`や`expect`などの記述が`no-undef`としてエラーとなる場合があります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/6158/c501e4a7-2256-3e89-dc6f-929bbcd85afd.png" width=600 />

これは、ESLintのJestプラグインを導入することで解決できます。

```bash
$ npm install --save-dev eslint-plugin-jest
```

`.eslintrc` に以下の設定を追加します。

```javascript:.eslintrc
{
  "env": {
    "jest/globals": true
  },
  "plugins": [
    "jest"
  ],
}
```

## テストファイルのマイグレーション
他のテストフレームワークからJestへ移行する場合、[マイグレーションガイド](https://facebook.github.io/jest/docs/en/migration-guide.html)を参考にすると良いでしょう。

ガイドでは[jest-codemods](https://github.com/skovhus/jest-codemods/)というツールが紹介されています。
このツールを使うとテスト記法を自動でJestに対応するように変換してくれるようです。

Jasmineからの移行の場合、ほぼ互換性があるようで、自分のプロジェクトでは既存のテストファイルに何も手をいれずともJestで動いてくれました。

## JSDOMに不足しているAPIをmockで補う
JestはNode.js上でブラウザ環境をエミュレートするために、[JSDOM](https://github.com/jsdom/jsdom)という実装を使っています。

このJSDOMはブラウザ環境のAPIを提供してくれているのですが、いくつか実装されていないAPIも存在するため、それらについてはmockで補う必要があります。

自分はテストを書くなかで、以下の不足しているAPIを見つけました。

- Web Storage API（local storage, session storage)
- window.getSelection

Web Storage APIはnpm packageでmockをインストールできます。

```bash
$ npm install --save-dev mock-local-storage
```

そして、インストールしたmockをimportするファイルを作成します。
さらに、`window.getSelection`のmockもこのファイルに記述します。

```javascript:jest-setup.js
import 'mock-local-storage';

// https://github.com/jsdom/jsdom/issues/937
window.getSelection = () => {
  return {
    addRange: () => {},
    removeAllRanges: () => {}
  };
};
```

作成したファイルをテスト起動時に読み込むように、以下の設定を`jest.config.js`（もしくはpackage.json）に追加します。

```javascript:jest.config.js
module.exports = {
  setupTestFrameworkScriptFile: '<rootDir>/jest-setup.js' // <- 追加
}
```

このようにすることで、テスト起動時にmockを読み込ませ、エラーとなることを防ぐことができます。

## おわりに
この記事で紹介した設定等はGithubに上げているので、よかったらこちらも見てやってください！
https://github.com/hogesuke/jest-demo