Help us understand the problem. What is going on with this article?

Jest + enzymeを導入する

More than 1 year has passed since last update.

初心者です🔰

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

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

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

1.png

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

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

Hitomi_Nagano
フロントエンドエンジニア
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away