2016年から始めるフロントサイド~テスト編~


## テスト

1. mocha
2. expect
3. jest
4. karma
5. enzyme

テスト用のライブラリは色々ありますが、React関係のプロジェクトでよく使われているものだけ取り上げます。

## mocha

* [公式サイト](https://mochajs.org/)
* es6でテストコードを書く場合は[babel-register](https://babeljs.io/docs/setup/#mocha)が必要です

```bash:インストール
$ npm install --save-dev mocha

# es6でテストを書く場合はインストール
$ npm install --save-dev babel-register
```

テストコードを書きます。

```js:test/calc.spec.js
import expect from 'expect'

describe('add', () => {
  
  let add
  beforeEach(() => {
    add = (x, y) => {
      return x + y
    }
  })
  
  afterEach(() => {
    add = () => {}
  })
  
  it('add(1, 2)', (done) => {
    expect(add(1, 2)).toEqual(3)
    done()
  })
})
```

あとはpackage.jsonのscriptでtestを追加して動かします。

```json:package.json
{
  ...,
  
  "scripts": {
    "clean": "rimraf lib dist",
    "compile": "babel --presets es2015 src/ -d lib/",
    "build": "cross-env NODE_ENV=production webpack",
    "build:dev": "npm run compile && npm run build",
    "lint": "eslint src",
    // mochaはデフォルトではtest/を探索するので、別のディレクトリを探索してほしいときは
    // --recursiveのあとにそのディレクトリを指定します
    // --compilersで指定のフォーマットにコンパイルしてくれます
    "test": "mocha --compilers js:babel-register --recursive"
  },
  
  ...,
  }
```

```bash:テスト
$ npm run test
```

## expect

* [公式サイト](https://github.com/mjackson/expect)
* assertionのライブラリです

```bash:インストール
$ npm install --save-dev expect
```

toBe, toEqualなどは直感的に使えそうなので、Spyを少し詳しく見てみる。

### [Spy](https://github.com/mjackson/expect/blob/master/modules/SpyUtils.js)

```js:createSpy
export function createSpy(fn, restore = noop) {
  if (fn == null)
    fn = noop

  assert(
    isFunction(fn),
    'createSpy needs a function'
  )

  let targetFn, thrownValue, returnValue

  const spy = function spy() {
    spy.calls.push({
      context: this,
      arguments: Array.prototype.slice.call(arguments, 0)
    })

    if (targetFn)
      return targetFn.apply(this, arguments)

    if (thrownValue)
      throw thrownValue

    return returnValue
  }
  
  spy.calls = []

  spy.andCall = (otherFn) => {
    targetFn = otherFn
    return spy
  }

  spy.andCallThrough = () =>
    spy.andCall(fn)

  spy.andThrow = (object) => {
    thrownValue = object
    return spy
  }

  spy.andReturn = (value) => {
    returnValue = value
    return spy
  }

  spy.getLastCall = () =>
    spy.calls[spy.calls.length - 1]

  spy.reset = () => {
    spy.calls = []
  }

  spy.restore = spy.destroy = restore

  spy.__isSpy = true

  spies.push(spy)
  
  return spy
}
```

createSpyでspyのconstructorを返す。引数にfnを指定するとaddCallThrough()でfnの返り値を取れる。addCall(fn)はspy作成後に好きなfunctionを設定できる。

```js:spyOn
export function spyOn(object, methodName) {
  const original = object[methodName]

  if (!isSpy(original)) {
    assert(
      isFunction(original),
      'Cannot spyOn the %s property; it is not a function',
      methodName
    )

    object[methodName] = createSpy(original, () => {
      object[methodName] = original
    })
  }

  return object[methodName]
}
```

object[methodName]をspyにする。addCallThrough()で元のfucntionが実行出来る。addCall(fn)で好きなfunctionに入れ替えられる。

一度試してみる。

```js:test/calc.spec.js
import expect, { createSpy, spyOn } from 'expect'

describe('add', () => {
  
  const calc = {
    add: () => {},
    multiply: () => {}
  }
  
  let add
  beforeEach(() => {
    add = (x, y) => {
      return x + y
    }
    calc.add = add
  })
  
  afterEach(() => {
    add = () => {}
    calc.add = add
  })
  
  it('add(1, 2)', (done) => {
    expect(calc.add(1, 2)).toEqual(3)
    done()
  })

  it('add spy', (done) => {
    // calc.addとspyは同じ参照を指す
    // calc.add(1, 2)はspy(1, 2)なのでspy.callsに結果がpushされる
    // その結果、spyがspy.callsから結果をのぞける
    let spy = spyOn(calc, 'add')
    expect(spy.andReturn(3).call(this)).toEqual(3)
    // 元のcalc.addを使う
    expect(spy.andCallThrough().call(this, 1, 2)).toEqual(3)
    // functionの入れ替え
    expect(spy.andCall((x, y) => {
      return x *2 + y * 2
    }).call(this, 1, 2)).toEqual(6)
    done()
  })
})
```

想定通りの結果となった。

## jest

* [公式サイト](https://facebook.github.io/jest/)
* facebook製のUnit testライブラリ。assertionにはjasmineというのを使用しているようだ。
* これ一つで何でもできるらしい。mocha+expect+karma相当？

```bash:インストール
$ npm install --save-dev jest-cli

# babelを使用する場合合わせてインストール
$ npm install --save-dev babel-jest babel-polyfill
```

npm runで実行できるようにする。
babelを使う場合はjestのpropertyを作成する。

```json:package.json
{
  ...,
  
  "scripts": {
    ...,
    
    "test": "mocha --compilers js:babel-register --recursive",
    "jtest": "jest"
  },
  // babelを使う場合に追加
  "jest": {
    "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
    "testFileExtensions": ["es6", "js"],
    "moduleFileExtensions": ["js", "json", "es6"]
  },
  
  ...,
}
```
`__tests__`ディレクトリを作成してテストを書く。

```js:__tests__/calc-test.js
// 特にexpectをimportする必要はない

describe('add', () => {
  
  const calc = {
    add: () => {},
    multiply: () => {}
  }
  
  let add
  beforeEach(() => {
    add = (x, y) => {
      return x + y
    }
    calc.add = add
  })
  
  afterEach(() => {
    add = () => {}
    calc.add = add
  })
  
  it('add(1, 2)', (done) => {
    expect(calc.add(1, 2)).toEqual(3)
    done()
  })
  /* expectしか使えない？
  it('add spy', (done) => {
    let spy = expect.spyOn(calc, 'add')
    expect(spy.andReturn(3).call(this)).toEqual(3)
    expect(spy.andCallThrough().call(this, 1, 2)).toEqual(3)
    expect(spy.andCall((x, y) => {
      return x *2 + y * 2
    }).call(this, 1, 2)).toEqual(6)
    done()
  })
  */
})
```

どうやらexpect()しか使えないようだ。。別のライブラリを使ってる可能性ありそう・・

```bash:テスト
$ npm run jtest
```

## karma

* [公式サイト](https://karma-runner.github.io/0.13/index.html)
* ブラウザ上でファイルを実行してくれる

```bash:インストール
# mochaとwebpackを使用することを想定
$ npm install --save-dev karma karma-chrome-launcher karma-mocha karma-webpack
```

configファイルを初期化します。

```bash:karma
$ ./node_modules/karma/bin/karma init karma.conf.js

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> mocha

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
>

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> test/**/*spec.js
>

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
>

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes
```

webpackでes6をコンパイルするので、configファイルを修正します。

```js:karma.conf.js
module.exports = function(config) {
  config.set({

    ...,

    // webpack指定
    preprocessors: {
      'test/**/*.js': ['webpack']
    },

    // es6をコンパイルするように設定
    webpack: {
      module: {
        loaders: [
          {
            test: /\.js$/,
            loaders: ['babel-loader?presets[]=react&presets[]=es2015&cacheDirectory'],
            exclude: /node_modules/
          }
        ]
      },
    },

    ...,
    
  })
}
```

npm runで実行できるようにする。

```json:package.json
{
  ...,
  
  "scripts": {
    ...,
    
    "test": "mocha --compilers js:babel-register --recursive",
    "jtest": "jest",
    "test-browser": "karma start"
  },
  
  ...,
}
```

```bash:テスト
$ npm run test-browser
```

## enzyme

* [公式サイト](http://airbnb.io/enzyme/)
* [5分でなんとなくわかる動画](https://www.youtube.com/watch?v=V5N0Ukb8LGg&list=PLb0IAmt7-GS0M8Q95RIc2lOM6nc77q1IY&index=35)
* Reactをテストすることに特化したTestライブラリ

```bash:インストール
$ npm install --save-dev enzyme

# 依存するライブラリのインストール
$ npm install --save-dev react-addons-test-utils react-dom
```

軽く試してみる。

```js:About.js
import React from 'react'

export default React.createClass({
  render() {
    return <div>About</div>
  }
})
```

```js:About.spec.js
import React from 'react'
import expect from 'expect'
import { shallow } from 'enzyme'
import About from '../src/react-router/modules/About'

describe('<About />', () => {
  
  it('should render there <About />', () => {
    const about = shallow(<About />)
    expect(about.find('div').length).toEqual(1)
  })
})
```

これでmochaを実行する。
アプリケーションがReact前提なら便利かも。

テストは色々なライブラリやフレームワークがあってどれを使うか悩みますね！