初心者です🔰
Jestとenzymeの理解を一番に深めてくれたのはYouTubeとUdemyかもしれません。
(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)
connect
、mapStateToProps
を入れるだけで上のテストの記述はエラーを出します。
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あたり使っていないので恐らく次回!)
(上の記述に間違いありましたら気軽にコメントください!)