🦑

React/Jestでのユニットテストに少し慣れてきたら役に立つtips

2023/01/05に公開
4

スペースマーケット所属のfumink7です。
北欧へのあこがれが高まっています☃️


ReactでのWebアプリケーション開発をはじめる中で、ユニットテストを書き始めたときに知って役立ったtipsをまとめてみました。

テスト環境
テスティングフレームワークはJest、UIテストのためにTesting Libraryを使用しています

  • typescript@4.9.4
  • React@18.2.0
  • jest@28.1.0
  • @testing-library/react@13.3.0

①アサーション

特定の要素内に絞って要素検索を行う - within

getBy、findByなどで「要素A内にある要素Bを取得する」場合にwithinを使って要素Aを指定することができます。

const formElement = screen.getByRole('form')

expect(
  within(formElement).getByRole('button', { name: 'XXボタン' }),
).toBeInTheDocument()

非同期処理を待ってからアサーションを行う - waitFor

非同期で何らかの処理を行った後アサーションを行う場合、非同期の処理が終わる前に実行されて期待した結果が得られない場合があります。
waitForを使うことで、非同期の処理が終わるのを待ってアサーションを行うことができます。

// 非同期での処理を行なう…

// 非同期処理の終了を待って引数に渡した処理を実行する
await waitFor(
  () => screen.getByRole('dialog', { name: 'XX確認' })
)

(追記)非同期処理を待ってから要素の取得を行うならfindBy

コメントいただいたのですが、単に非同期処理を待って要素を取得するのであればfindByを利用するほうが簡潔に書けますね。

findBy methods are a combination of getBy queries and waitFor.

// 非同期処理の終了を待って要素を取得する
await screen.findByRole('dialog', { name: 'XX確認' })

これでよさそう

曖昧な値の比較

expectの対象が「数字であれば何でもいい」「オブジェクトがこの値を含んでいればあとはなんでもいい」みたいな場合の書き方です。

何かしらの値(型)であることを確認したい

// 関数がXXを引数として実行されたことをテスト

// なんでもいいから引数が渡された
expect(funcMock).toBeCalledWith(expect.anything())

// 数値が引数として渡された
expect(funcMock).toBeCalledWith(expect.any(Number))

ArrayやObjectが対象の値を含んでいるかどうか

// someArrayに'hoge'が含まれていること
expect(someArray).toEqual(
  expect.arrayContaining([ 'hoge' ]),
)

// someObjectにname(文字列)が含まれていること
expect(someObject).toEqual(
  expect.objectContaining({ name: expect.any(String) }),
)

テキストの部分一致を確認

// 正規表現にマッチするかどうか
expect(somePathText).toEqual(
  expect.stringMatching(/\/hogehoge\/fugafuga\/$/),
)

// 文字列にhogehogeを含んでいるか
expect(someText).toEqual(expect.stringContaining('hogehoge'))

余談:いろんな場面で正規表現が使える

正規表現で思い出しましたがTesting Libratyでテキストを扱うものは大抵(?)正規表現が使えます。便利ですね〜

// テキストで要素を探すときにも、テキストを渡すマッチャーでも使える
expect(screen.getByText(/正規表現/)).toHaveTextContent(/正規表現/)

アサーションにスナップショットを使う - toMatchInlineSnapshot

意図した変更が含まれていないことを確認するスナップショットテストですが、レンダリングしたコンポーネント全体を比較するテストだけではなく、特定の値に変更が入っていないかどうかを確認するアサーションで使えるスナップショット機能があります。

expect(someObject).toMatchInlineSnapshot()

// ↑の状態でテストを実行すると、取得された値が引数に自動で記録される↓

expect(someObject).toMatchInlineSnapshot(`
  Object {
    hoge: 'fugafuga' 
  }
`)

次回以降テスト実行時にsomeObjectが違う値になっている場合テストが失敗します。

タグの属性をテストする - toHaveAttribute

HTML要素の属性の値もtoHaveAttributeで簡単にテストできます。

// 取得した要素のaria-hiddenの値をテストする
const text = screen.getByText('description')
expect(text).toHaveAttribute('aria-hidden', 'false')

Promiseでの非同期処理のエラーテストの書き方のバリエーション

Promiseでの非同期処理においてエラーが発生することを確認するテストは

await expect(promiseInstance).rejects.toThrowError()

と書くのが一般的ですが、アサーションって一般的に実行結果を何かと比較するというものが多いので、なんか上記の書き方だとぱっと理解がしにくいんですよね(そんなことない?)

チームで話していたときに、下記のような書き方のほうがわかりやすいのでは?という意見が出ました。

const errorObject = await promiseInstance().catch(
  (error) => error,
)
expect(errorObject).toEqual(expect.any(Error))

エラーが発生するのならば、実行した際にcatchの処理が走ってerrorObjectが返ってくるよね、というテストのほうがわかりやすいのではという話になったのですが、みなさんはどうでしょうか??

②Mock

モジュールを部分的にMockしたい - requireActual

importするモジュールをmockする際、requireActualを使うことで一部はMock、それ以外はもとのままimportするということが可能になります。

const someFuncMock = jest.fn(() => false)
jest.mock('../someModulePath', () => {
  const actual = jest.requireActual('../someModulePath')
  return {
    ...actual,
    someFunc: someFuncMock,
  }
})

// 「XXXの時、someFuncMockが実行される」テスト内でのアサーション
expect(someFuncMock).toBeCalledTimes(1)

※jest.spyOnでも同じようなことができそう

import * as someModule from "../some-module-path";

// テスト内
const spySomeFunc = jest.spyOn(someModule, "someFunc");
expect(spySomeFunc).toBeCalledTimes(1)

③APIレスポンスのMock(msw)

(記事紹介)mswを使ったネットワークレベルでのMock

フロントエンド開発では必ず発生するであろうAPIへのリクエスト(REST、GraphQL)ですが、mswを使うことで簡単にリクエストを行うロジックのテストを書くことができます。

mswでは指定したエンドポイントへのリクエストに対するレスポンスをMockとして作成できるので、リクエストを送るところ&レスポンスが帰ってきた後をテストコードで再現できるのがありがたいですよね。

実際の使用イメージに関してはチームメンバーが書いている記事を紹介させていただいて。
https://note.com/gota_disney/n/n76924efedfaa?magazine_key=m5c0d938cd0df

Zennにも記事が沢山
https://zenn.dev/topics/msw

fetch is not defined - fetchはnodeに存在しない

さあこれでAPIリクエストのテストが書けるぞ!と意気込んでみたはいいものの、fetchを利用している場合にテスト実行するとfetch is not definedと怒られてしまいます。。

それもそのはず、node.jsにはfetchが存在しないので実行できません。

mswのissueを参考するに、「テストのためにfetchを用意するのはテスティングライブラリの責任から外れるので、自分でポリフィルを行ってね」とのことだそう。

スレッド内ではnode-fetchwhatwg-fetchなどが挙がっていましたが、弊社ではisomorphic-fetchを使っています。
テストのセットアップファイルなどでimportしておくことでfetchが実行できるようになります。

resetHandlersでデフォルト状態に戻す

特定のテスト実行時にMockを変更して、そのテストが終わればMockを元に戻して次のテストを実行したいときはresetHandlersで初期化してあげましょう。

// mswを使って、指定したリクエスト実行時に返ってくる値をmockできる
// resetHanlersを実行するとこの状態に戻る
const server = setupServer(
  rest.post('http://hogehoge/fugafuga', (req, res, ctx) =>
    res(
      ctx.json(
        // 省略
      ),
    ),
  ),
)

beforeAll(() => {
  server.listen()
})
// テスト毎に初期化
beforeEach(() => {
  server.resetHandlers()
})
afterAll(() => {
  server.close()
})

description('いくつかのテスト', () => {
  test('テスト1', () => {
    // デフォルト状態でテスト実行
  }
  test('テスト2', () => {
    server.use(
      rest.post('http://hogehoge/fugafuga', (req, res, ctx) =>
        res(ctx.status(500),
	ctx.json(
	  // 省略
	)
      ),
    )
    // ↑変更されたMockでテスト実行
  }
  test('テスト3', () => {
    // リセットされるのでデフォルト状態でテスト実行
  }
}

③その他

複数テストパターンをまとめて実行 - test.each

値の組み合わせを変えて複数パターンまとめてテストを行いたいという場合はeachを使うと簡潔にかけて便利です。

// 各パラメータと実行結果を各変数にセットにして、5パターンのテストを実行する
test.each`
  typeName1    | value1     | typeName2    | value2        | expected
  ${'猫'}       | ${cat}    | ${'犬'}       | ${dog}       | ${-1}
  ${'猫'}       | ${cat}    | ${'うさぎ'}     | ${rabbit}    | ${-1}
  ${'猫'}       | ${cat}    | ${'猫'}       | ${cat}       | ${0}
  ${'犬'}       | ${dog}    | ${'猫'}       | ${cat}       | ${1}
  ${'うさぎ'}    | ${rabbit}  | ${'猫'}       | ${cat}        | ${1}
`(
  // テスト名に各値を含めることができる
  '第一引数が $typeName1 、第二引数が $typeName2 の場合 $expected が返却されること',
  ({ value1, value2, expected }) => {
    const actual = someFunction(value1, value2)
    expect(actual).toBe(expected)
  },
)

// テスト名の例:
// 第一引数が 猫 、第二引数が 犬 の場合 -1 が返却されること

(最後に宣伝)スペースマーケットはエンジニアを募集しています!

月並みですが、、、エンジニア大募集中です!
現在(2023/01)ではインフラ、モバイルアプリ、バックエンドを中心に募集しているので、もし興味ある方いらっしゃればこちら覗いてみてください〜

https://www.wantedly.com/projects/1113570
https://www.wantedly.com/projects/1113544
https://www.wantedly.com/projects/1061116

スペースマーケット Engineer Blog

Discussion

fumink7fumink7

確かに非同期処理待って要素を取得するのはfindByのほうが簡潔に書けますね!
ありがとうございます、追記しておきます〜😄

クロパンダクロパンダ

(だいぶ端折ってコメントしてしまってすいません) たしか expect().toBeInTheDocument で囲まなくても同じ動作をしたと思います

(追記: 改めて調べてみてますが囲むかどうかで微妙に挙動が違うかもしれないです)

fumink7fumink7

あ、たしかに要素見つからなければテスト失敗しますもんね。
簡潔にexpectで囲まないように修正しておきます!