コネヒト開発者ブログ

コネヒト開発者ブログ

Jest + react-testing-library でフロントエンドテストをコツコツ積み上げている話

こんにちは。コネヒト歴7ヶ月目のWebエンジニアの古市です。

私の所属するチームではReactで構築されたCMSを開発しています。 Atomic Designに則り、コンポーネントを Atoms/Molecules/Organisms/Pagesの区分で作成しています。このうち、Atoms,Molecules,OrganismsについてはJest+react-testing-libraryの組み合わせで必ずテストを書くようにしています。 今回は実際に書いているテストコードを例に挙げながら、どのような点をテストコードで担保しているか、また、テストを積み重ねるための施策について説明いたします。

具体的なテストコード

これは業務で書いているテストコードを抽象化した一例です。 以下のような構造のコンポーネントのテストだとイメージしていただければと思います。

  • 名前が表示される
  • アバター画像が表示される
  • コメントを記入するinputと、付随するラベルが存在する
  • 「更新」と書かれたボタンを押下で変更内容をupdateする
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { SomeComponent } from 'components/organisms/SomeComponent'
import client from 'api/client' // 実装したAPI

// post時のAPIをモック化
jest.mock('api/client')
const mockedAPI = client.post as jest.Mock

// コンポーネントに投入する初期モックデータ
const mockData: MockData = {
  id: 123,
  name: 'コネヒト太郎',
  image: 'https://some-url/320x480.png',
  label: 'テキスト',
  comment: '',
}

まずテストを書くために必要なライブラリとテスト対象のコンポーネントをimportします。 そしてコンポーネントに流し込むモックデータを一番最初に定義します。

// describe -> test の順番で記述 
describe('<SomeComponent />', () => {
  test('should render component', () => {
    const mock: MockData = mockData

    render(<SomeComponent mockData={mock} onClickUpdate={() => {}} />)

    // 名前、アバター画像、ラベルの描画チェック
    expect(screen.getByText(mock.name)).toBeInTheDocument()
    expect(screen.getByRole('image')).toBeInTheDocument()
    expect(screen.getByLabelText(mock.label)).toBeInTheDocument()

    // textareaの初期値チェック
    expect(screen.getByRole('textbox', { name: 'テキスト' })).toHaveDisplayValue(mock.comment)
    // ボタンの描画チェック
    expect(screen.getByRole('button', { name: '更新' })).toBeInTheDocument()
  })

最初にコンポーネントが描画されることをテストコードで確認します。 testing-libraryを使う場合、screen.getBy*というQueryメソッドでDOM要素の有無を特定することが定石ですが、アクセシビリティに則り使用する優先順位が以下のように定められています。

  1. getByRole
  2. getByLabelText, getByPlaceholderText
  3. getByText, getByDisplayValue

テストコードでDOM要素を特定するときも、アクセシビリティの観点からなるべくこの優先順位を無視しないよう心がけています。(どうしても難しい時にtestByIdなどを活用します。) 他にも、書くメソッドの使用優先順位が定められているので詳しくは公式サイトのリファレンスをチェックしてください。

また、過去に自身でQueryの優先順位について整理してLTで発表したスライドもあるので、こちらもぜひご覧いただけると幸いです。

React Testing Library の Query について整理してみた - Speaker Deck

それではテストの続きです。 2番目のテストスイートでは要素の変更が反映されるかをチェックしていきます。

  test('Events should be called', () => {
    const onClickUpdate = jest.fn()
   
    render(<SomeComponent mockData={mock} onClickUpdate={onClickUpdate} />)

    // テキストアイテムのテキスト変更のonChangeイベントをテスト
    const textareaContent = screen.getByRole('textbox', { name: 'テキスト' })
    fireEvent.change(textareaContent, { target: { value: 'テキストを変更しました' } })
    expect(screen.getByDisplayValue('テキストを変更しました')).toBeInTheDocument()

    // 更新ボタンのonClickをテスト
    fireEvent.click(screen.getByRole('button', { name: '更新' }))
    expect(onClickUpdate).toHaveBeenCalled()
  })

「更新」をボタン押下したときに呼び出されるメソッドはjest.fn()でモックしておきます。 textboxへの新しい値の入力や、ボタンのクリックなどのイベントはfireEventメソッドでモックし、伝達することができます。

最後に、テキストの更新が意図通り行われた時、APIにリクエストが送られているかをテストします。

  test('Save API should be called', async () => {
    mockedAPI.mockResolvedValueOnce({
      status: 200,
      data: {
        item: { ...mock, comment: 'テキストを変更しました' },
      },
    })

    const mockUpdated = { ...mock, comment: 'テキストを変更しました' }
    const onClickUpdate = jest.fn()

    render(<SomeComponent mockData={mockUpdated} onClickUpdate={onClickUpdate} />)

    // テキストアイテムの文言を変更
    fireEvent.click(screen.getByRole('button', { name: '更新' }))
    await waitFor(() => expect(mockedAPI).toHaveBeenCalledTimes(1))

    expect(screen.getByDisplayValue(mockUpdated.comment)).toBeInTheDocument()
  })
})

コンポーネントに変更後の comment を流し込み、更新ボタンを押した時に、テストコードの冒頭でモック化したAPIが呼び出され、正常なステータスと更新後の値がコンポーネントに反映されているかを上記でテストしています。モック化したAPIを叩く時は、waitFor(() => ...の前にawaitを記述しないと正しく結果が得られません。 以上は一例ですが、正常系のテスト以外でも、コンポーネントによっては4xxのバリデーションエラーや5xxのサーバーエラー発生時の挙動をテストコードで補完する場合があります。

テストを積み重ねるための工夫

開発者がテストを必ず書くことを促すため、プロジェクトのリポジトリ内にCodecovをGitHub ActionのWorkflowに導入しています。Pull Requestを送信するとリポジトリの最新(main)と当該PRのdiffを視覚的に把握することができます。また、最低カバレッジ率を .yml ファイル内に記載することができます。 pages以外のコンポーネントを作成してPull Requestを送信した時、テストが書かれていないまたはテストケースが足りていない場合に、Codecovがカバレッジ低下の警告を出し、テストケースを追加すべき場所にコメントを自動的に付けます。

  • Codecovが表示するdiff

  • テストが不足している場合に出す警告のコメント

上記のようにカバレッジが下がっている状態ではCIが通過せず、コードレビューに提出することができません。独力で正確にテストコードを追加できない場合には他のメンバーにアドバイスをもらったり、モブプロ / ペアプロで解決させるように取り組んでいます。現時点ではカバレッジ率を90%にしていますが、これをもう1段階高く設定することを目標にしています。

終わりに

テストコードの解説が大半を占めてしまいましたが、チーム内でのフロントエンドテストへの取り組みについて説明いたしました、まだJestやtesting-libraryの使い方でつまづく時があるため、社内で知見を共有しあい、今後も真摯にテストに向き合いつつツールへの習熟度を高めていきたいと思います。最後までお読みいただきありがとうございました。

PR

コネヒトでは React を使ってテストも書きたいエンジニアを募集しています!

hrmos.co