環境構築
今回は Create React App を使用します。
Create React App には初期状態でJestが組み込まれているのですぐに使うことができます。
$ yarn create react-app jest-test --template typescript $ cd jest-test
シンプルなTypeScriptファイルをテスト
Reactの前にシンプルなTypeScriptファイルをテストしてみます。
ユニットテストを最初に試すのに単純な足し算関数が例に挙げられることが多いですが、ここでも例にならって単純な足し算をする関数を作ってみましょう。
src/utils/sum.ts
const sum = (number1: number, number2: number): number => { return number1 + number2 } export default sum
次にテストファイルを作ります。
テストファイルを作る場所はテストするファイルと同階層に置くか、__tests__
ディレクトリの中に配置することが多いようです。
また、ファイル名に関しては.test.ts
かspec.ts
という拡張子で作成します。
今回は__tests__/sum.test.ts
というディレクトリとファイル名にします。
sum
の引数に「2,3」を渡すと「5」が返ることをテストコードを書いてみましょう。
src/__tests__/sum.test.ts
import sum from 'utils/sum' test('合計値が正しい', () => { expect(sum(2, 3)).toBe(5) })
これでテストをする準備ができました。
Create React App では下記コマンドを実行してテストします。
$ yarn run test
これは想定通りの値なのでテストはpassedになるはずです。
試しに値を変更してfailedになることも確認してみてください。
基本的に一つの機能に対し複数のテストを書くことになると思います。
その場合describe
でまとめることができます。
describe('sumのテスト', () => { test('合計値が正しい', () => { expect(sum(2, 3)).toBe(5) }) test('合計値が正しくない', () => { expect(sum(2, 3)).not.toBe(6) }) })
ちなみに値がイコールでない場合をテストするにはnot
を使用します。
Reactコンポーネントのテスト
次はコンポーネントをテストしてみます。
例えばこんな感じのコンポーネントがあるとします。
src/components/Header.tsx
import React from 'react' type Props = { title?: string } const Header: React.FC<Props> = ({ title }) => { return ( <header> <h1>{title || 'React App'}</h1> </header> ) } export default Header
title Propsがある場合は変数を表示して、ない場合はデフォルトの文字列を表示するというシンプルなコンポーネントです。
コンポーネントをテストするには@testing-library/react
をインポートします。
テストを書く前にscreen.debug()
を実行してみてください。
src/__tests__/Header.test.tsx
import { render, screen } from '@testing-library/react' import Header from 'components/Header' test('Propsがない場合はデフォルト文字列が表示される', () => { render(<Header />) screen.debug() })
実際にどのように表示されるかタグが出力されると思います。
一部だけ確認したい場合はlogDOM
が使えます。
import { logDOM, render, screen } from '@testing-library/react' import Header from 'components/Header' test('Propsがない場合はデフォルト文字列が表示される', () => { render(<Header/>) logDOM(screen.getByText('React App')) })
想定通りならこの結果をexpect
すればいいだけですね。
では、React Appが表示されるというテストコードを書いてみます。
test('Propsがない場合はデフォルト文字列が表示される', () => { const { container } = render(<Header />) expect(container.innerHTML).toMatch('React App') })
container.innerHTML
でHTMLが取れるので、toMatch
で指定した文字列が含まれるかテストしています。
Propsがある場合のテストは次のようになります。
test('Propsがある場合は変数が表示される', () => { const title: string = 'テスト' const { container } = render(<Header title={title} />) expect(container.innerHTML).toMatch(title) })
React Hooksのテスト
Hooksはコンポーネントでしか実行できないのでテストするのはちょっとややこしそうですね。
これも@testing-library/react
を使うと簡単にテストできます。
テストするのはこんな感じのよくあるカウンターフックです。
src/hooks/counter.ts
import { useState } from 'react' export type returnType = { count: number increment: () => void decrement: () => void } export const useCounter = (): returnType => { const [count, setCount] = useState<number>(0) const increment = () => setCount(count + 1) const decrement = () => setCount(count - 1) return { count, increment, decrement } }
Hooksをテストするには@testing-library/react
のrenderHook
とact
を使用します。
HooksにはrenderHook
を通してアクセスして、メソッドを実行するときはact
の中で実行します。
src/__tests__/counter.test.ts
import { renderHook, act } from '@testing-library/react' import { useCounter } from 'hooks/counter' describe('Counter Hooks', () => { test('インクリメントされる', () => { const { result } = renderHook(() => useCounter()) act(() => result.current.increment()) expect(result.current.count).toBe(1) }) test('デクリメントされる', () => { const { result } = renderHook(() => useCounter()) act(() => result.current.decrement()) expect(result.current.count).toBe(-1) }) })
イベントテスト
さきほど作ったカウンターフックをコンポーネントに組み込んでみました。
src/__tests__/counter.test.ts
import React from 'react' import { useCounter } from 'hooks/counter' const Counter: React.FC = () => { const { count, increment, decrement } = useCounter() return ( <div> {count} <button onClick={increment}>+</button> <button onClick={decrement}>-</button> </div> ) } export default Counter
ボタンをクリックするとカウントされるコンポーネントです。
次はこのイベントをテストしてみます。
イベントをテストするには@testing-library/user-event
のuserEvent
を使用します。
src/__tests__/Counter.test.tsx
import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import Counter from '../components/Counter' test('インクリメントされる', () => { render(<Counter />) userEvent.click(screen.getByRole('button', {name: '+'})) expect(screen.getByText(1)).toBeInTheDocument() })
現在 Create React App でインストールされる@testing-library/user-event
はバージョン13ですが、14からは記述方法が変わっています。
test('インクリメントされる', async () => { const user = userEvent.setup() render(<Counter />) const button = screen.getByRole('button', {name: '+'}) await user.click(button) expect(screen.getByText(1)).toBeInTheDocument() })
setup
が必要だったりasync
で書かないといけなかったりでちょっと記述が増える感じですね。
Mockを使用したテスト
次にテストするのはDateを渡すと、その日が過去の日付の場合trueを未来ならfalseを返す関数です。
src/utils/isPast.ts
export const isPast = (date: Date): boolean => { return date.getTime() < Date.now() }
ここまでくればtrue,falseをテストするのは楽勝ですね。
ちなみにtoBeTruthy
がtrue
、toBeFalsy
がfalse
であることテストできます。
src/__tests__/isPast.test.ts
import { isPast } from 'utils/isPast' describe('isPast util', () => { test('過去の日付の場合はtrue', () => { const date = new Date('2022-04-30') expect(isPast(date)).toBeTruthy() }) test('未来の日付の場合はfalse', () => { const date = new Date('2022-06-30') expect(isPast(date)).toBeFalsy() }) })
しかし、このテストコードには問題があります。
テストコードを書いた時点では「2022-06-30」は未来の日付で問題なくても、「2022-06-30」を過ぎた後テストを実行すると失敗してしまいます。
この問題を解決するにはモックという機能を使用します。
モックを使用することで関数の戻り値を固定にすることができます。
簡単なコードで試してみましょう。
test('Mock test', () => { const spy = jest.spyOn(Date, 'now') spy.mockReturnValue(new Date('2021-05-20').getTime()) console.log(new Date(Date.now())) })
ログに表示されたのは現在の日時ではなくmockReturnValue
で設定した「2021-05-20」になったと思います。
これを使って先ほどのテストを書き直してみます。
複数のテストの前に同じ処理を実行したい場合はbeforeEach
を使用します。
import { isPast } from 'utils/isPast' describe('isPast util', () => { beforeEach(() => { const spy = jest.spyOn(Date, 'now') spy.mockReturnValue(new Date('2021-05-01').getTime()) }) test('過去の日付の場合はtrue', () => { const date = new Date('2021-04-30') expect(isPast(date)).toBeTruthy() }) test('未来の日付の場合はfalse', () => { const date = new Date('2021-06-30') expect(isPast(date)).toBeFalsy() }) })
このように実行した日時や外部の要因によってテスト結果が変わってしまうような処理はモックを使用することで解決することができます。