Jest ＋ enzymeを導入する

初心者です🔰


Jestとenzymeの理解を一番に深めてくれたのはYouTubeとUdemyかもしれません。
- Youtube
[React Redux Unit & Integration Testing with Jest and Enzyme](https://www.youtube.com/watch?v=EgJZv9Iyj-E)

- Udemy
[React Testing with Jest and Enzyme](https://www.udemy.com/react-testing-with-jest-and-enzyme/learn/v4/overview)

（Udemyのリンクも貼りましたが、上記コースはcreate-react-appのバージョンが古いためエラーが出てしまいます。また、上に貼ったYoutubeは作成途中ですので、まだReduxを実装した場合のユニットテスト系は現在公開されてません。(2019/02/15時点)Jestとenzymeに関する動画はたくさん上がっています！）


参考にさせていただいた記事も載せます。

- [jestのgithubにあるexampleたち](https://github.com/facebook/jest/tree/master/examples)

- [enzyme公式サイト](https://airbnb.io/enzyme/)

- [Jest + enzymeで行うReactのユニットテストについて](https://mae.chab.in/archives/60066)

- [How to Test Async Redux Thunks](https://glennstovall.com/how-to-test-async-redux-thunks/)



今回使ったライブラリはこちらです。

- jest-enzyme
- enzyme
- enzyme-adapter-react-16
- moxios
- redux-mock-store


##導入前準備

上記のライブラリをインストールして、package.jsonに以下の記述を足します。

```json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --verbose --coverage"
    ...
  }

```

##まずはsetupTests.jsを作成

srcディレクトリ配下に`setupTests.js`を新規作成し、以下のコードを書きます。

```javascript
import Enzyme from 'enzyme'
import EnzymeAdapter from 'enzyme-adapter-react-16'

Enzyme.configure({
  adapter: new EnzymeAdapter(),
  disableLifecycleMethods: true
})
```

setupTests.jsを認識してもらうのと、絶対パスの設定を`jest.config.js`を作成して以下のように書きました。

jest.config.js

```javascript
module.exports = {
  moduleNameMapper: {
    '^@app/(.+)': '<rootDir>/src/js/$1', // 絶対パス
  },
  verbose: true,
  setupFilesAfterEnv: ['<rootDir>/src/js/setupTests.js'], // setupTests.jsの置き場所
}
```


##簡単な記述から始めます！

実は基本のキの段階で躓きまくってました。
Material-UIのコンポーネント`AppBar`が存在するかどうかで記述ミス連発してました。
まずはMaterial-UIのコンポーネントが存在するかの記述を書きます。

```javascript
import * as React from 'react'
import { createShallow, createMount } from '@material-ui/core/test-utils'
import Header from '@app/components/Header'

describe('<Header />', () => {
  let shallow
  let mount
  beforeEach(() => {
    shallow = createShallow()
    mount = createMount()
  })

  it('AppBarが読み込まれている', () => {
    const wrapper = mount(<Header />)
    expect(wrapper.find('AppBar').length).toEqual(1)
  })
})
```

なんてことない記述なのですが、`createShallow`, `createMount`に辿り着くまでや、`shallow`でなく`mount`の方を使うところに至るまで結構時間が掛かっています。。
（言い訳ですが「Material-UI jest enzyme test」でググっても理解できなかったのです…！）

これで一旦`AppBar`が表示されている記述は終わったのですが、
私はHeaderコンポーネントにReducerで管理しているステートを呼び出しています。

```javascript
import React from 'react'
import { connect } from 'react-redux'
import AppBar from '@material-ui/core/AppBar'

class Header extends React.Component<*> {
  render() {
    return (
      <div>
        <AppBar position="sticky" color="inherit">
          Something
        </AppBar>
      </div>
    )
  }
}

const mapStateToProps = state => ({
  isAuthenticated: state.auth.isAuthenticated,
})
export default connect(mapStateToProps)(Header)
```

`connect`、`mapStateToProps`を入れるだけで上のテストの記述はエラーを出します。


```text
Invariant Violation: Could not find "store" in the context of "Connect(Header)". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to Connect(Header) in connect options.
```

なので、これを回避して再度AppBarが読み込まれているテストをしていきます。

##storeにアクセスできるようにする

この方法はUdemyで紹介されたものを少しだけ自分用に変えたものです。

まずは`__tests___`フォルダの外に「`testUtils`」ディレクトリを作成して、その中に`testUtils.js`を作成します。

その中に以下のような記述をします。

```javascript
import { createStore, applyMiddleware } from 'redux'
import rootReducer from '@app/reducers'
import thunk from 'redux-thunk'

/**
 * @function storeFactory
 * @param {object} initialState
 * @returns {Store} Redux Store
 */
export const storeFactory = initialState => {
  const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
  return createStoreWithMiddleware(rootReducer, initialState)
}
```

私は今回Middlewareに`redux-thunk`を使いましたので上のような記述にしています。



エラーが出てしまったHeader.test.jsに`storeFactory`関数を呼び出します。

```javascript
import * as React from 'react'
import { createShallow, createMount } from '@material-ui/core/test-utils'
import { storeFactory } from '@app/testUtils/testUtils'
import { Provider } from 'react-redux'
import Header from '@app/components/Header'

describe('<Header />', () => {
  let shallow
  let mount
  beforeEach(() => {
    shallow = createShallow()
    mount = createMount()
  })

  it('AppBarが読み込まれている', () => {
    const initialState = {}
    const store = storeFactory(initialState)
    const wrapper = mount(
      <Provider store={store}>
        <Header />
      </Provider>
    )
    expect(wrapper.find('AppBar').length).toEqual(1)
  })
})
```

```javascript
const store = storeFactory(initialState)
const wrapper = mount(
  <Provider store={store}>
    <Header />
  </Provider>
)
```

を追加しただけです。これでエラーが消え、AppBarの存在についてのテストはパスしました！

……まだAppBarの存在を確認しただけ…！

実はHeaderコンポーネントの挙動は、
ユーザー認証して、Reduxで管理している`this.props.isAuthenticated`ステートがtrueの時`AppBar`を表示＆falseの時は消えるように記述しています。

なのでpropsの値によってAppBarの存在をテストしていきます！

##propsの値を参照してテストする

`storeFactory` の引数に渡していた`initialState`に、reducerで管理しているステートを渡してあげます。
ユーザー認証で管理している`auth`ステートは以下のような情報を持たせています。

```javascript
const INITIAL_STATE = {
  oauthStatus: {},
  userStatus: {},
  loading: false,
  error: null,
  isAuthenticated: false,
}
```

AppBarの存在を確認するのに条件２つ、
①`isAuthenticated: true`  → AppBar表示
②`isAuthenticated: false` → AppBar非表示

なので、これを書いていきます。


```javascript
import * as React from 'react'
import { createShallow, createMount } from '@material-ui/core/test-utils'
import { storeFactory } from '@app/testUtils/testUtils'
import { Provider } from 'react-redux'
import { BrowserRouter as Router } from 'react-router-dom'
import Header from '@app/components/Header'

describe('<Header />', () => {
  let shallow
  let mount
  beforeEach(() => {
    shallow = createShallow()
    mount = createMount()
  })

  it('認証済みの時AppBarが表示されている', () => {
    const initialState = {
      auth: {
        oauthStatus: {},
        userStatus: {},
        loading: false,
        error: null,
        isAuthenticated: true,
      },
    }
    const store = storeFactory(initialState)
    const wrapper = mount(
      <Provider store={store}>
        <Router>
          <Header />
        </Router>
      </Provider>
    )
    expect(wrapper.find('AppBar').length).toEqual(1)
  })

  it('未認証の時AppBarが表示されていない', () => {
    const initialState = {
      auth: {
        oauthStatus: {},
        userStatus: {},
        loading: false,
        error: null,
        isAuthenticated: false,
      },
    }
    const store = storeFactory(initialState)
    const wrapper = mount(
      <Provider store={store}>
        <Router>
          <Header />
        </Router>
      </Provider>
    )
    expect(wrapper.find('AppBar').length).toEqual(0)
  })
})
```


wrapperを定義している記述が一緒なので、`beforeEach`内に書いていってスッキリさせます！
新しく`setup()`関数を作り、そこに`storeFactory()`とwrapperの定義を書いています。

出来上がりはこんな感じです。

```javascript
import * as React from 'react'
import { createShallow, createMount } from '@material-ui/core/test-utils'
import { storeFactory } from '@app/testUtils/testUtils'
import { Provider } from 'react-redux'
import { BrowserRouter as Router } from 'react-router-dom'
import Header from '@app/components/Header'

const shallow = createShallow()
const mount = createMount()

const setup = (state = {}) => {
  const store = storeFactory(state)
  const wrapper = mount(
    <Provider store={store}>
      <Router>
        <Header />
      </Router>
    </Provider>
  )
  return wrapper
}

describe('<Header />', () => {
  it('認証済みの時AppBarが表示されている', () => {
    const initialState = {
      auth: {
        oauthStatus: {},
        userStatus: {},
        loading: false,
        error: null,
        isAuthenticated: true,
      },
    }
    const wrapper = setup(initialState)
    expect(wrapper.find('AppBar').length).toEqual(1)
  })

  it('未認証の時AppBarが表示されていない', () => {
    const initialState = {
      auth: {
        oauthStatus: {},
        userStatus: {},
        loading: false,
        error: null,
        isAuthenticated: false,
      },
    }
    const wrapper = setup(initialState)
    expect(wrapper.find('AppBar').length).toEqual(0)
  })
})
```

問題なくテストがパスしました！

【終えて】
テストは色々面倒ですが、テストにパスしてターミナルが緑色で満たされると普通に嬉しいです。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/327745/a1e079fd-9341-1627-8a9f-c2d62562ecea.png)

まだ他のコンポーネントでテストを実施していくので、今回は導入のドで締めます。
（まだmockあたり使っていないので恐らく次回！）

（上の記述に間違いありましたら気軽にコメントください！）