Jest+EnzymeでReactコンポーネントをテストしようと思ったらshallow
がuseContext
に対応していなかったので、その対処法のメモです。
結論
useContext
をカスタムhookでラップして、spyOn
でモック化する
説明
うまくいかないパターン
StudentListという、生徒の情報一覧を表示するコンポーネントを想定します。
export interface Student {
id: number
name: string
gender: 'male' | 'female'
}
import React from 'react'
import { Student } from './types'
interface ProviderProps {
students?: Student[]
}
export const StudentsContext = React.createContext<Student[]>([])
export const StudentsContextProvider: React.FC<ProviderProps> = ({students = [], ...props}) => (
<StudentsContext.Provider value={students} {...props} />
)
import React from 'react'
import { StudentsContext } from './StudentsContext'
export const StudentList: React.FC = () => {
const students = React.useContext(StudentsContext)
return (
<ul>
{students.map(student => (
<li key={student.id}>
{student.name}({student.gender})
</li>
))}
</ul>
)
}
これをテストしてみます。
import React from 'react'
import { shallow } from 'enzyme'
import { StudentList } from './StudentList'
import { StudentsContextProvider } from './StudentsContext'
import { Student } from './types'
const studentsVal: Student[] = [
{ id: 1, name: 'name1', gender: 'male' },
{ id: 2, name: 'name2', gender: 'female' },
]
describe('StudentList component', () => {
it('should render all students', () => {
const wrapper = shallow(
<StudentsContextProvider students={studentsVal}>
<StudentList />
</StudentsContextProvider>
)
const items = wrapper.dive().dive().find('li')
expect(items.length).toBe(2)
})
})
コケました
● StudentList component › should render all students
expect(received).toBe(expected) // Object.is equality
Expected: 2
Received: 0
StudentList.tsx
内のReact.useContext(StudentsContext)
が正しく実行できていないことが原因です。
うまくいくパターン
useContext
の処理を、カスタムhookでラップして、そのカスタムhookをモック化する形で対応していきます。
import React from 'react'
import { Student } from './types'
interface ProviderProps {
students?: Student[]
}
const StudentsContext = React.createContext<Student[]>([])
// useStudentsContextというカスタムhookでuseContextをラップする
export const useStudentsContext = () => React.useContext(StudentsContext)
export const StudentsContextProvider: React.FC<ProviderProps> = ({students = [], ...props}) => (
<StudentsContext.Provider value={students} {...props} />
)
import React from 'react'
import { useStudentsContext } from './StudentsContext'
export const StudentList: React.FC = () => {
// useContextではなくカスタムhookで取得するようにする
const students = useStudentsContext()
return (
<ul>
{students.map(student => (
<li key={student.id}>
{student.name}({student.gender})
</li>
))}
</ul>
)
}
import React from 'react'
import { shallow } from 'enzyme'
import { StudentList } from './StudentList'
// spyOnに渡すために`* as ~`の形でインポート
import * as StudentsContextObj from './StudentsContext'
import { Student } from './types'
const studentsVal: Student[] = [
{ id: 1, name: 'name1', gender: 'male' },
{ id: 2, name: 'name2', gender: 'female' },
]
describe('StudentList component', () => {
it('should render all students', () => {
// useStudentsContextをモック化
jest.spyOn(StudentsContextObj, 'useStudentsContext')
.mockImplementation(() => studentsVal)
// そしてもはやProviderも不要になる
const wrapper = shallow(<StudentList />)
const items = wrapper.find('li')
expect(items.length).toBe(2)
})
})
これでテストが通るようになります。
spyOn
について
ドキュメント:
https://deltice.github.io/jest/docs/ja/jest-object.html#jestspyonobject-methodname
spyOn
の第一引数はObject、第二引数はそのメソッドなので、それに合わせて
import * as StudentsContext from './StudentsContext'
のような形でインポートするところがポイントです。
ちなみに今回の場合、useStudentsContext
のカスタムhookを作らなくても
jest.spyOn(React, 'useContext').mockImplementation(() => studentsVal)
で代替できますが、この場合当然全てのuseContext
がモック化されてしまうので、避けたほうが良いでしょう。