はじめに
reactで画像のスライドを実装したくて
swiperを導入したところjestのテストが動かなくなって困ったので記事にしました。
背景
ReactプロジェクトはCRA(Create-React-App)で作成。
各verは以下
react: 18.2.0
jest:27.5.2
typescript: 4.9.5
swiper:10.1.0
何に困ったのか
swiperによる実装は問題なくできたが
jestでテストを動かすとエラーが出るようになリました。
swiperがver7以降commonJSからESMに切り替わったため
jest環境ではswiperがimportできずエラーが出ます
テスト対象のコードが以下です
import React from 'react';
import './App.css';
import { Swiper, SwiperSlide } from 'swiper/react'
import { Navigation, Pagination } from 'swiper/modules'
import 'swiper/css/bundle'
import 'swiper/css/pagination'
import 'swiper/css/navigation'
function App() {
const swiperOptions = {
pagination: true,
centeredSlides: true,
modules: [Navigation, Pagination],
spaceBetween: 10,
}
return (
<div className="App">
<Swiper {...swiperOptions}>
<SwiperSlide>
<img
src='/images/azarashi.png'
alt="original1"
/>
</SwiperSlide>
<SwiperSlide>
<img
src='/images/ika.png'
alt="original2"
/>
</SwiperSlide>
</Swiper>
</div>
);
}
export default App;
テストが以下です(レンダーしてるだけなので落ちるはずがないテストです)
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
it('renders learn react link', () => {
render(<App />);
});
発生したエラーが以下です
● Test suite failed to run
Cannot find module 'swiper/react' from 'src/App.tsx'
Require stack:
src/App.tsx
src/App.test.tsx
1 | import React from 'react';
2 | import './App.css';
> 3 | import { Swiper, SwiperSlide } from 'swiper/react'
| ^
4 | import { Navigation, Pagination } from 'swiper/modules'
5 | import 'swiper/css/bundle'
6 | import 'swiper/css/pagination'
at Resolver.resolveModule (node_modules/jest-resolve/build/resolver.js:324:11)
at Object.<anonymous> (src/App.tsx:3:1)
at Object.<anonymous> (src/App.test.tsx:3:1)
原因
jestがESMであるSwiperを見つけることができないのが問題でした。
最初はjest.mockでモックしとけば大丈夫でしょって軽く考えて
jest.mock('swiper/react')って書いてみたんですがエラーは変わりませんでした。
おそらく node_modules/swiper/package.json のexportsで定義している
エントリーポイントが読み取れてない?のだと思われます。jestの仕様なのかな?
詳しいことはわからないです。
解決策
jestの設定ファイルを変更してテストの時は自作したモックファイルを参照するようにします。
CRAで作成したプロジェクトにおいてjestの設定を変えるにはejectしなければいけないのですが
ejectすると元に戻せないなどデメリットも多かったので今回はcracoでjestの設定をオーバーライドすることにします。
cracoとは?
→Create React App Configuration Override の略。
CRAの設定をejectせずにカスタマイズするためのライブラリです。
①マニュアルモックの作成
node_modulesと同じ階層に
__mocks__ディレクトリを作成します。
その中に以下のようにファイルを配置してください。
__mocks__
┗swiper
┗react.js
┗modules.js
┗css
┗bundle.js
┗navigation.js
┗pagination.js
各ファイルの中身は以下です
module.exports = {
Swiper: () => 'Swiper',
SwiperSlide: () => 'SwiperSlide',
}
ここでreturnしているSwiperとかSwiperSlideとかは実際は何でもいいです。
とりあえずSwiperとSwiperSlideをexportしてればOK。
module.exports = {
Navigation: () => 'Navigation',
Pagination: () => 'Pagination',
}
bundle.js,navigation.js,pagination.jsは
cssファイルの代わりでテストの時にファイルが存在してればいいので中身は空でOKです。
②cracoのインストール
npm install --dev @craco/craco
③cracoの設定ファイルを作成
ファイル名は craco.config.js です
package.jsonと同じ階層に配置してください。
module.exports = {
jest: {
configure: {
moduleNameMapper: {
'^swiper/react$': '<rootDir>/__mocks__/swiper/react.js',
'^swiper/modules$': '<rootDir>/__mocks__/swiper/modules.js',
'^swiper/css/bundle$': '<rootDir>/__mocks__/swiper/css/bundle.js',
'^swiper/css/pagination$':
'<rootDir>/__mocks__/swiper/css/pagination.js',
'^swiper/css/navigation$':
'<rootDir>/__mocks__/swiper/css/navigation.js',
},
},
},
}
ここで実装で参照しているパスに対してテストの時に参照するファイルを指定するマッピングを行います。
実装では以下の参照をしているので
import { Swiper, SwiperSlide } from 'swiper/react'
import { Navigation, Pagination } from 'swiper/modules'
import 'swiper/css/bundle'
import 'swiper/css/pagination'
import 'swiper/css/navigation'
importでswiper/react のパスが指定された時は
<rootDir>/__mocks__/swiper/react.js
のファイルを参照する。って感じです。
実装でimportしているswiper関連のファイル全てにマッピングしてあげます。
^swiper/react$
この記述で正規表現で完全一致の指定になります。
④package.jsonの編集
cracoでオーバーライドした設定を使ってテストを実行したいので
package.jsonのscriptsを書き換えます。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
下記のようにcracoを使用したテストにコマンドを書き換えます。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "craco test",
"eject": "react-scripts eject"
}
以上でjest環境での実行時swiper関連のファイルはマニュアルモックが参照されるようになりました。
テストを実行すると問題なく実行できると思います
おまけ
swiperの中にimgが2つ配置されていることを確認したい場合は上記の設定を行ったのち、
テストファイル内で以下のようにjest.mockを記述します。
jest.mock('swiper/react', () => ({
Swiper: ({ children }: { children: ReactNode }) => <div>{children}</div>,
SwiperSlide: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))
これでSwiperとSwiperSlideのchildrenはそのまま表示されるようになりテストが容易になります。
(swiperの機能が無視された状態です。)
テストファイルは以下のようになります
import React, {ReactNode} from 'react'
import {render, screen} from '@testing-library/react'
import App from './App'
jest.mock('swiper/react', () => ({
Swiper: ({ children }: { children: ReactNode }) => <div>{children}</div>,
SwiperSlide: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))
it('original1とoriginal2が配置されていていること', () => {
render(<App />)
expect(screen.getByAltText('original1')).toBeInTheDocument()
expect(screen.getByAltText('original2')).toBeInTheDocument()
});
最後に
この問題について色々調べましたがどこにも答えがなく、なんとか捻り出した解決策になります。
今回はswiperでしたが他のESMのライブラリにも応用が効く方法だと思いますので参考にしてみてください。
何か誤り等ありましたらコメントでお願いします。