React周辺は周りの誰にも聞けないし本当に手探りで色々やってます。
いいかげんフロントでもテストを導入しないと意味不明になってきたので、まずはReact自体のテストを学習します。
基本的にはJest公式などを参考にしながら進めています。
環境構築
プロジェクトの作成
この辺はすっ飛ばしても良さそうですが、今回は学習ということで新規にプロジェクトを作成していきます。
いつもはNext.jsを利用していますが、極力シンプルに進めたいので通常のReactプロジェクトを作成していきます。
npx create-react-app react-test-learning --template typescript
とりあえず実行しておきます
cd react-test-learning yarn start
いつものくるくるReactマークが出ればOKです。
ロジックのテスト
簡単なテスト
まずはReactのテストというよりJavaScriptのテストを行ってみます。
srcディレクトリにsum.ts
とsum.test.ts
を追加します。
sum.ts
export const sum = (a: number, b: number): number => { return a + b }
sum.test.ts
import {sum} from "./sum" test("add 1 + 2 to equal 3", () => { expect(sum(1,2)).toBe(3) })
これでとりあえずテストは回る筈です。
yarn test
でJestを実行します。
PASS src/sum.test.ts √ add 1 + 2 to equal 3 (2 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.888 s, estimated 4 s Ran all test suites related to changed files.
とりあえず良さそうです。
クリーンなプロジェクトだとTypeScriptを利用しているのでBabelの設定などいるんでしょうが、create-react-appを利用してプロジェクトを作成しているのでその辺はよしなにしてくれているっぽいです。
ちゃんとしたテスト入門であればexpect
のMatcher
を色々見ていくところですが、その辺はやりながら覚えることにします。
基本に立ち返る際にはJestの公式を参照したいところです。
非同期のテスト
非同期のコードが含まれているテストにはテストに渡す関数の引数に与えられるdone
を使うかPromiseで処理するかのどちらかだそうです。
おそらくナウいのはPromiseだと思いますし、今は async/await を多用すると思うのでその辺で試してみます。
めんどくさいので先ほどのsum.ts
に追記します。
sum.ts
~ export const promiseFunction = (value: number) => { return new Promise((resolve)=>{ setTimeout(() => { resolve(value) }, 1000) }) }
sum.test.ts
import {sum, promiseFunction} from "./sum" ~ test("async test", async () => { const value = await promiseFunction(100) expect(value).toBe(100) })
Jestを実行します。
PASS src/sum.test.ts √ add 1 + 2 to equal 3 (1 ms) √ async test (1000 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 3.8 s Ran all test suites related to changed files.
ちゃんとasyncの方はsetTimeoutで設定した1000msかかってます。
describeブロックとかスコープの話
簡単なテストであれば上記までのtest
で処理出来そうですが、もう少し大がかりなテストになってくるとブロック化してテストを回したくなるはずです。(例えば、どこかに通信して取得したデータに対して、それぞれ処理を行うモジュールなど)
テストファイルのグローバルにbeforeEach
やbeforeAll
などを記載することも出来ますが、describe
ブロックに分けて実行する方がシンプルだと思います。
セットアップとティアダウンに関しては公式の情報が参考になると思います。
testとit
ウェブ上ではit
を利用しているテストもありましたが、testはitのエイリアスなので全く同じモノのようです。
ただ公式ドキュメントでもitを利用してテストを書いているケースはないので可読性?からかtestを利用する方がよさそうです。
Reactのテスト
スナップショットテスト
JavaScript(TypeScript)のテストは上記までのもので大丈夫そうではある(モックとかあるけどその辺は後回し)ので、Reactのテストを行っていきます。
まずはレンダリングがうまくいってるか分かりやすいスナップショットテストを行ってみます。
create-react-appでプロジェクトを作成していると、App.tsx
とApp.test.tsx
が自動で作られているのでコレを使い回します。
テストするコードはJestの公式を参考に、react-test-renderer
を使用しない形でスナップショットを取っています。
App.tsx
import React, {useState} from 'react'; import logo from './logo.svg'; import './App.css'; type Status = "normal" | "hovered" const App = () => { const [status, setStatus] = useState<Status>("normal") const onMouseEnter = () => { setStatus("hovered") } const onMouseLeave = () => { setStatus("normal") } return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.tsx</code> and save to reload. </p> <a className={status} href="https://reactjs.org" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > Learn React </a> </header> </div> ) } export default App
App.test.tsx
import React from 'react' import { render, screen, fireEvent } from '@testing-library/react' import App from './App' test('changes the class when hovered', () => { const testComponent = render(<App />) expect(testComponent).toMatchSnapshot() fireEvent.mouseEnter(screen.getByRole("link")) expect(testComponent).toMatchSnapshot() });
Jestを実行すると、src
ディレクトリ下に__snapshots__
ディレクトリが作成され、スナップショットが保存されていると思います。
とはいえ、この辺は個人的にはStorybookのVisual Testingで良い気がするのでたぶん使わないと思います。
レンダリングテスト
データなどを受信したり非同期処理を行った後に正しくレンダリングされているかテストします。
とはいえこの辺は実際に作って見て考える要素が大きいので、ここではTesting Libraryの公式例を挙げておくにとどめておきます。
基本的にはロジックのテストと同様に行いますが、下記に注意しながらテストを組み立てていくようです。
- 検証したい要素をTesting Libraryの
screen
のメソッドのgetByText
やgetByRole
などで取得して評価する - イベントの発火はTesting Libraryの
fireEvent
を利用して発火させる - イベントの発火の影響を待つ場合はTesting Libraryの
waitFor
を利用してawaitで待つ
という感じのようです。
もしテストに慣れて記事に出来る程度に知識がついたら別の記事にするかも知れません。