Edited at

Jest + enzymeを導入する

初心者です🔰

Jestとenzymeの理解を一番に深めてくれたのはYouTubeとUdemyかもしれません。

- Youtube

React Redux Unit & Integration Testing with Jest and Enzyme

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

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

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


  • jest-enzyme

  • enzyme

  • enzyme-adapter-react-16

  • moxios

  • redux-mock-store


導入前準備

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

{

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


まずはsetupTests.jsを作成

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

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

module.exports = {

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


簡単な記述から始めます!

実は基本のキの段階で躓きまくってました。

Material-UIのコンポーネントAppBarが存在するかどうかで記述ミス連発してました。

まずはMaterial-UIのコンポーネントが存在するかの記述を書きます。

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で管理しているステートを呼び出しています。

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)

connectmapStateToPropsを入れるだけで上のテストの記述はエラーを出します。

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を作成します。

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

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関数を呼び出します。

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)
})
})

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ステートは以下のような情報を持たせています。

const INITIAL_STATE = {

oauthStatus: {},
userStatus: {},
loading: false,
error: null,
isAuthenticated: false,
}

AppBarの存在を確認するのに条件2つ、

isAuthenticated: true → AppBar表示

isAuthenticated: false → AppBar非表示

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

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の定義を書いています。

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

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)
})
})

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

【終えて】

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

まだ他のコンポーネントでテストを実施していくので、今回は導入のドで締めます。

(まだmockあたり使っていないので恐らく次回!)

(上の記述に間違いありましたら気軽にコメントください!)