Webpack依存のReactコンポーネントをテストする

React Advent Calendar 8日目の記事です。

#はじめに
Reactを使用する場合はそれ単体で使用するのは珍しく、WebpackやBrowserifyといったビルドツールを使用すると思います。今回はWebpackに依存したReactコンポーネントのテスト方法について書きます。
本記事で使用したコードは[こちら](https://github.com/chimame/babel-plugin-webpack-loaders-example)から参照可能です。
ただ、**本記事は`babel`にどっぷりですので、その点が無理な方は回れ右です**。

#前提条件
本記事ではテストで[Jest](https://github.com/facebook/jest)を使用する前提で書いております。

#主な使用ライブラリ
Webpack:1.13.3
React:15.4.1
Jest:17.0.3

#テスト対象コード
```js:components/sample/index.js
import React from 'react'
import style from './style.css'
import Config from 'Config'

export default () => {
  return(
    <ul className={style.styletest}>
      <li>first element</li>
      <li>second element</li>
      <li>{Config.thirdElementName}</li>
    </ul>
  )
}
```

```css:components/sample/style.css
.styletest li{
  margin-right: 10px;
}
```

```js:config/webpack.dev.config.js
var webpack = require('webpack')
var postcssImport = require('postcss-import')
var autoprefixer = require('autoprefixer')
var precss = require('precss')

module.exports = {
  entry: './app/index',
  output: {
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loaders: ['babel-loader']
      },
      {
        test: /\.css$/,
        include: /app/,
        loaders: [
          'style-loader',
          'css-loader?modules',
          'postcss-loader'
        ]
      }
    ]
  },
  postcss(webpack) {
    return [
      postcssImport({
          addDependencyTo: webpack
      }),
      precss,
      autoprefixer
    ]
  },
  resolve: {
    extensions: ['', '.js', '.css']
  },
  devServer: {
    hot: true,
    port: 4000,
    inline: true,
    historyApiFallback: true
  },
  externals: {
    'Config': JSON.stringify({
      thirdElementName: 'third'
    })
  }
}
```

さて今回テストするは`components/sample/index.js`です。この中にはWebpackに依存したコードが2パターン存在します。
まず、1つ目ですが、`import style from './style.css'`です。これはWebpackのloaderに依存しています。
2つ目ですが、`import Config from 'Config'`と`Config.thirdElementName`部分です。これはWebpackのexternalsに依存しています。
このようにWebpackに依存していると以下のJestのテストコードを動かすと

```js:__test__/sample.js
import sample from '../sample'

describe('toMatchSnapshot example', () => {
  test('render sample', () => {
    expect(sample()).toMatchSnapshot()
  })
})
```

![image](https://qiita-image-store.s3.amazonaws.com/0/70363/757cf7f6-4139-a13e-57cc-4495a0b0796b.png)

`SyntaxError: Unexpected token`という風にエラーが表示されて動作しません。
そこで今回はこの2つのWebpack依存のコードを解消し、テストする方法例を書きます。

#loader依存を解消
こいつは結構簡単で[JestのWebpackページ](https://facebook.github.io/jest/docs/tutorial-webpack.html)に色々とヒントが書いてあります。
方法は大きく分け2つあります。1つ目は「loader部分をモックしてしまう方法」、2つ目は「loader部分を事前にBabel pluginで変換してしまう方法」があります。
せっかくなので両方紹介したいと思います。

##loaderをモックする方法
**※※テストするだけであればこちらの方法を強く推奨します※※**
モックには[identity-obj-proxy](https://github.com/keyanzhang/identity-obj-proxy)というpackageを使用します。

###identity-obj-proxyのインストール
いつもどおり下記のコマンドでインストールします。

```
$ npm install -D identity-obj-proxy
```

###環境の設定
Jestで使用する場合は以下のようにpackage.jsonに設定を追記し、`identity-obj-proxy`の使用設定します。

```json:package.json
{
...
  "jest": {
    "moduleNameMapper": {
      "\\.(css|less)$": "identity-obj-proxy"
    }
  }
}
```

見ればわかると思いますが、`.css`もしくは`.less`のimport部分をモックしてくれるようになります。たったこれだけでOKです。

##loaderを事前に変換する方法
loader部分の変換には[babel-plugin-webpack-loaders](https://github.com/istarkov/babel-plugin-webpack-loaders)というbabelのプラグインを使用します。

###babel-plugin-webpack-loadersのインストール
まずはbabel-plugin-webpack-loaders(babel-cliも)を入れます。

```
$ npm install -D babel-cli babel-plugin-webpack-loaders
```

###環境の設定
次に設定ファイルを編集していきます。まずは`.babelrc`を以下のように編集します。

```diff:.babelrc
{
  "presets": ["es2015", "react"],
+  "env": {
+    "test": {
+      "plugins": [
+        [
+          "babel-plugin-webpack-loaders",
+          {
+            "config": "config/webpack.test.config.js",
+            "verbose": false
+          }
+        ]
+      ]
+    }
+  }
}
```

追記したのは`env`以降です。環境変数の`NODE_ENV`が`test`の場合はインストールしたプラグインを使用する設定にしています。
次に記載した`config/webpack.test.config.js`を追加します。

```js:config/webpack.test.config.js
module.exports = {
  output: {
    libraryTarget: 'commonjs2',
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loaders: ['babel-loader']
      },
      {
        test: /\.css$/,
        include: /app/,
        loaders: [
          'style-loader',
          'css-loader?modules',
          'postcss-loader'
        ]
      }
    ]
  },
  resolve: {
    extensions: ['', '.js', '.css']
  },
}
```

必要なのは`libraryTarget: 'commonjs2', `とloaderの記述です。これでloader部分を変換してくれるようになります。
次に`package.json`のテスト実行コマンド部分を以下のように書き換えます。

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

`BABEL_DISABLE_CACHE`と`NODE_ENV`の環境変数を設定しておきます。

##解消の確認
どちらの方法を使用してもいいですが、テスト結果が以下のように変わるはずです。

![image](https://qiita-image-store.s3.amazonaws.com/0/70363/bbe5247a-70fd-8983-3b09-bdc49820f4dc.png)

エラーの内容が`externals`依存の「Configなんてものが読めない」ってエラーに変わりました。

#externals依存を解消
次にexternalsの解消です。
これはそもそもWebpackから剥がしてやります。
じゃあENVごとの設定はどうするのかというと[babel-plugin-transform-define](https://github.com/FormidableLabs/babel-plugin-transform-define)を使って剥がします。

##babel-plugin-transform-defineのインストール
お決まりの`npm install`です

```
$ npm install -D babel-plugin-transform-define
```

##環境の設定
まずは`webpack.dev.config.js`の`externals`部分を`app.dev.config.js`というのを作って切り出します。

```js:config/app.dev.config.js
"use strict"

module.exports = {
  'Config': {
    thirdElementName: 'third'
  }
}
```

次に`.babelrc`に`babel-plugin-transform-define`の設定を書きます。

```diff:.babelrc
{
  "presets": ["es2015", "react"],
  "env": {
+    "development": {
+      "plugins": [
+        ["transform-define", "config/app.dev.config.js"]
+      ]
+    },
    "test": {
      "plugins": [
        [
          "babel-plugin-webpack-loaders",
          {
            "config": "config/webpack.test.config.js",
            "verbose": false
          }
        ],
+        ["transform-define", "config/app.dev.config.js"]
      ]
    }
  }
}
```

`NODE_ENV`が`development`や`test`の場合に読み込むファイルを切り替えることができます。（今回は手を抜いて同じものを読み込んでいます。
最後に`components/sample/index.js`の依存部分を削除しておきます。

```js:components/sample/index.js
import React from 'react'
import style from './style.css'
//import Config from 'Config' deleted

export default () => {
  return(
    <ul className={style.styletest}>
      <li>first element</li>
      <li>second element</li>
      <li>{Config.thirdElementName}</li>
    </ul>
  )
}
```

##解消の確認
これでテストを動かすと
![image](https://qiita-image-store.s3.amazonaws.com/0/70363/4fad9e93-2e0b-9b79-7fad-ccc7f8d803b4.png)

テストが正常に実行でき、完了できました。externals依存の解消は若干例が悪いですが、**実コードで使用するとすれば、APIのリクエスト先であるendpointを環境ごとに変える場合とかに設定したりするのではないでしょうか。**

#さいごに
なぜ上記のように対応したかですが、テストを動作させるのにわざわざWebpackでビルドするのもどうなんだろうと思い、上記のように対応した次第です。
もし同じようなことで悩んでいる方の助けになれば幸いです。

明日のReact Advent Calendar 2016 9日目は @noradaiko さんによる 「remark + ReactでMarkdownをレンダリングする」 です。