🐶

Reactのテストをちょっとよくするかもしれない細いTips

2023/10/03に公開1

こんにちは。株式会社スペースマーケットでフロントエンドエンジニアをしておりますwado63です。
以前弊社の記事でReactのテストの実践的なTipsを紹介していましたが、もっと細かい、テストをちょっとよくするかもしれないTipsを紹介したいと思います。

Mock関数に型を当てる

jestのnamespaceには、jest.MockedFunctionという型があります。
これを使うことでMock関数に元の関数の型を当てることができます。

export const myFunction = (num) => num**2  // 数字を2乗する関数
import { myFunction } from './myFunction'

jest.mock('./myFunction', () => ({
  myFunction: jest.fn()
}))

const mockedMyFunction = myFunction as unknown as jest.MockedFunction<typeof myFunction>

// myFunctionの返り値はnumber型なので、string型を返すとエラーになる
mockedMyFunction.mockReturnValue("hoge")

TypeScriptを使っていると実装のインターフェイスが変わった時点でそれを使用している箇所の型エラーが出ますが、Mockに型を当てていない場合は実行するまでそのMockのエラーに気づくことができません。

静的解析が効くのでMock関数にも型を当てておくと便利です。

繰り返し使うArrangeはhelper関数にまとめる

フロントエンドのテストコードを書く際にMSWを使うことはよくあると思いますが、
テスト毎にAPIの振る舞いを変えるようにテストを書くと以下のようになります。

import { rest } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer()

beforeAll(() => {
  server.listen()
})
AfterEach(() => {
  server.resetHandlers()
})
afterAll(() => {
  server.close()
})

test("TestA", async () => {
  // Arrange
  server.use(
    rest.get('/user/todos', (req, res, ctx) => {
      return res(ctx.status(200), ctx.json([
        { id: 1, title: 'todo1' },
      ]))
    })
  )

  // Act
  // Assert
})

testB("TestB", async () => {
  // Arrange
  server.use(
    rest.get('/user/todos', (req, res, ctx) => {
      return res(ctx.status(200), ctx.json([
        { id: 1, title: 'todo1' },
        { id: 2, title: 'todo2' },
      ]))
    })
  )

  // Act
  // Assert
})

テストが1〜2個だけであれば問題ないですが、テストケースが増えれば増えるほど、
Arrangeの部分の行数やネストが可読性を下げてきます。

ということでAPIの値をセットするところまでをhelper関数にまとめてしまいましょう。

import { rest } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer()

beforeAll(() => {
  server.listen()
})
AfterEach(() => {
  server.resetHandlers()
})
afterAll(() => {
  server.close()
})

const setUserTodosResponse = (todos: {
  id: number
  title: string
}[]) => {
  server.use(
    rest.get('/user/todos', (req, res, ctx) => {
      return res(ctx.status(200), ctx.json(todos))
    })
  )
}

test("TestA", async () => {
  // Arrange
  setUserTodosResponse([
    { id: 1, title: 'todo1' },
  ])

  // Act
  // Assert
})

test("TestB", async () => {
  // Arrange
  setUserTodosResponse([
    { id: 1, title: 'todo1' },
    { id: 2, title: 'todo2' },
  ])

  // Act
  // Assert
})

これでArrangeの部分の見通しが良くなり、どのような値をセットしているかということだけに集中できます。
今回はtodoの型をその場で作ってしまいましたが、何かしらschemaから型を作るなりしてもいいですね。

MSWのコードを自動生成するようなライブラリもありますが、それだけだと引数の部分を毎回書くことになるので、
個人的にはそのような場合でもhelper関数を用意してあげた方がテストコードは見やすくなると思います。

Custom Render関数を作る

Contextを使ったコンポーネントをテストする際に、毎回Providerをラップするのは面倒と思ったことはありませんか。testing-libraryが提供してくれるrenderをoverrideして、あらかじめProviderをラップした状態でrenderしておくとテストが書きやすくなります。

こちらのドキュメントで紹介されています。

https://testing-library.com/docs/react-testing-library/setup/

import React from 'react'
import {render} from '@testing-library/react'
import {ThemeProvider} from 'my-ui-lib'
import {TranslationProvider} from 'my-i18n-lib'
import defaultStrings from 'i18n/en-x-default'

const AllTheProviders = ({children}) => {
  return (
    <ThemeProvider theme="light">
      <TranslationProvider messages={defaultStrings}>
        {children}
      </TranslationProvider>
    </ThemeProvider>
  )
}

const customRender = (ui, options) =>
  render(ui, {wrapper: AllTheProviders, ...options})

// re-export everything
export * from '@testing-library/react'

// override render method
export {customRender as render}

testEachでassertionをまとめる

jestにはtest.eachという関数があります。
値の組み合わせを用意してまとめてテストできるというものですね。

test.each`
  num | expected
  ${1} | ${1}
  ${2} | ${4}
  ${3} | ${9}
`("与えられた引数を2乗すること", ({ num, expected }) => {
  expect(myFunction(num)).toBe(expected)
})

testing-libraryコンポーネントのテストで、ある境界値を境に表示・非表示を変えるようなテストを書く場合は、
screen関数と合わせて条件ごとにassertionをまとめて書けたりします。

import { render, screen } from '@testing-library/react'
import { MyComponent } from './MyComponent'

test.each`
  condition | assertion
  ${1} | ${() => expect(screen.getByRole('button', {name: "click here"}).toBeInTheDocument())}
  ${2} | ${() => expect(screen.getByRole('button', {name: "click here"}).toBeInTheDocument())}
  ${3} | ${() => expect(screen.queryByRole('button', {name: "click here"}).not.toBeInTheDocument())}
  ${4} | ${() => expect(screen.queryByRole('button', {name: "click here"}).not.toBeInTheDocument())}
`('条件が2以下の際はボタンが表示されないこと', ({
  condition,
  assertion
}) => {
  render(<MyComponent condition={condition} />)
  assertion()
})

便利だなと思う反面、Arrange, Act, Assertionの順番が変わってしまうのと、
test.eachの行が長く読みづらくなってしまいがちなので、多用せずにここぞというように使うと良いかと思います。

最後に

以前にもテストまわりのTips的な記事を書いておりますので、よろしければこちらもご覧いただければ幸いです。

https://zenn.dev/spacemarket/articles/2b62549286086f
https://zenn.dev/spacemarket/articles/57de17c53d8f15

スペースマーケット Engineer Blog

Discussion

masashimasashi

記事見ました!
大変勉強になります!
testEachでassertionをまとめるところですが、私がよく使っている方針を共有します

describe("与えられた引数を2乗すること", () -> {
  it.each([
  [1, 1],
  [2, 4],
  [3, 9],
  ])("%nを計算すると%nとなる", ({ num, expected }) => {
  expect(myFunction(num)).toBe(expected)
  })
})

この方法のメリットは、型のサポートが受けられることと、個別の単体テストの意味が通りやすくなることにあります
TypeScriptで書いている場合は型のサポート恩恵は大きいと思いますので、こちらのスタイルを共有しました!