Open16

React テスト

bz0bz0

https://zenn.dev/tkdn/books/react-testing-patterns

なぜテストをするか

  • 開発者が安心して開発するためといった理由が大きい(誰のため = 自分のため)
  • サービスの品質を担保するもの、バグがない保証を示すもの、といった標準化・エビデンスの類ではない
  • システムのサービスレベルとして評価、アウトプットの標準化などは受け入れのための試験項目で担保すべき
  • 開発において繰り返されるフィードバックサイクルに耐えうるため
  • 実装から読み取りづらい意図をテストコードがフォロー
  • 継続してテストコードが書けていれば意図しない変更でミスがあった場合に指摘してくれる

テスト方針

  • テストがソフトウェアの利用方法に似ているほど信頼性が得られる
  • 実装の詳細をテストしない(してもOK)
  • Web アプリケーションがどのようにユーザーに見えるか・操作されるかをトレースできるテストを書く
  • まず動作が不安定・心配で眠れないコンポーネントに小さくテストコードを書いてみる

環境構築

下記参考に行う
https://qiita.com/keitakn/items/0a714997eb058f2f67e2

$ yarn add jest ts-jest @testing-library/react @testing-library/react-hooks @types/jest jest-fetch-mock --dev

テスト実行

npm run test

参考

Reactディレクトリ構成例
https://qiita.com/tk-r1d3r/items/aec311c78846f7c9bf04
Next.js使ったプロジェクトでUnitテストを書く
https://qiita.com/Slowhand0309/items/335689e0e966a16b6447

bz0bz0

「 SyntaxError: Invalid or unexpected token」は、ファイルの最初の行がCSSクラス宣言であるためエラーが起きている。styleMock.jsを作って空のオブジェクトに変換してやることでエラーを回避する。
https://stackoverflow.com/questions/54627028/jest-unexpected-token-when-importing-css

$ npm run test

> narou-front@0.1.0 test /home/kz/wk/narou-front
> jest

 FAIL  src/__tests__/index.test.tsx
  ● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:

    /home/kz/wk/narou-front/node_modules/tailwindcss/tailwind.css:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){@tailwind base;
                                                                                      ^

    SyntaxError: Invalid or unexpected token

      1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
    > 2 | import 'tailwindcss/tailwind.css';
        | ^
      3 | import GlobalNav from '../components/atoms/globalnav';
      4 | import Card from '../components/atoms/card';
      5 | import React, { useState, useEffect, useReducer, useRef } from "react"

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1728:14)
      at Object.<anonymous> (src/pages/index.tsx:2:1)
bz0bz0

「The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.
Consider using the "jsdom" test environment.」というエラー発生。

jest.config.jsで「testEnvironment: 'jsdom'」とすることでエラー解消

https://qiita.com/mame_daifuku/items/79b6a5a1514a3f067e1a

$ npm run test

> narou-front@0.1.0 test /home/kz/wk/narou-front
> jest

 FAIL  src/__tests__/index.test.tsx
  Books
    ✕ render (2 ms)

  ● Books › render

    The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.
    Consider using the "jsdom" test environment.
    
    ReferenceError: document is not defined

      21 |     };
      22 |
    > 23 |     const { asFragment } = render(<Books bookResult={BookResult} genreResult={GenreResult} />)
         |                                  ^
      24 |     expect(asFragment()).toMatchSnapshot()
      25 |   })
      26 | })

      at render (node_modules/@testing-library/react/dist/pure.js:83:5)
      at Object.<anonymous> (src/__tests__/index.test.tsx:23:34)
bz0bz0

Warning: Each child in a list should have a unique "key" prop.
ユニークなキーが必要とのことなので、mapメソッドのコールバック関数の第二引数(index)をキーとして付与することで解決

配列に値がない場合にも起きる為、データがない場合の処理を分岐させる

https://techtechmedia.com/unique-key-prop-react/

bz0bz0
  • テストグループ:describe("グループ名", () => {/* ... */})
  • afterEach:テストケース終了毎に React コンポーネントを画面から unmount するためのクリーンナップ用ヘルパー関数である、cleanup() が実行される

スナップショットテスト

最終的な関数の出力である DOM を静的なファイルとして書き出すことで次回テスト時に変更検知ができる

  • Jest によってスナップショットファイルは初回実行時に別途格納される
  • 次回以降正しい変更をしたかは開発者が都度判断しスナップショットのアップデートをしていく必要がある
test("render", () => {
  const { asFragment } = render(<Counter />)
  expect(asFragment()).toMatchSnapshot()
})

debug

コンポーネントのHTMLを確認できる

screen.debug();

アサーション関数

  • getBy,queryBy,findByの使い分け
    • 要素が存在しないことをアサーションする場合にはqueryBy
    • それ以外の場合は標準でgetBy
    • 最終的に存在する非同期要素に使うfindBy
  • toBeInTheDocument:ドキュメント内に存在するか確認
  • getByText
    • テキストで検索(正規表現も可能のようだが自分が試した限りでは上手く動かず)
    • 要素が取得できなかった時はエラーをスロー
    • expectで明示的なアサーションを書く代わりに暗黙的なアサーションとして利用する人もいる
    • textでの検索は取りたい要素がちゃんと取れるか不安な側面あり使いづらい気もするがどうなんだろう
  • getByRole:aria-label属性で要素を取得する
    • 利用できないroleを選択すると代案をサジェストしてくれる
  • getByTestId:ソースコードのHTMLにdata-testid属性を指定してその要素を取得
    • テスト用のidというのが明確に分かるのでclassやidで取得するよりいいと思った(削除するときに影響範囲気にしなくていい)

環境変数

https://blog.shinki.net/posts/nextjs-jest-environment-variable
https://maku.blog/p/gbpeyov/

bz0bz0

非同期API(async/await)のテスト
プルダウン変更(select=combobox)のタイミングで非同期APIでデータを取り直して表示する場合のテストです。
非同期APIで取得されるデータをモックにしています。

  test("ジャンルを変更した場合", async () => {
    //APIから帰ってくる値のモック
    const BookResult:BookResult = {
      message: "ok",
      count: 0,
      data: []
    };
  
    const dataBooksMock = () =>
      new Promise((resolve) => {
        resolve({
          ok: true,
          status: 200,
          json: async () => (bookResult)
        })
      })
    global.fetch = jest.fn().mockImplementation(dataBooksMock)
  
    render(<Books bookResult={BookResult} genreResult={GenreResult} />)
    //プルダウンの変更
    const select = screen.getByRole('combobox')
    
    await waitFor(() => {
      fireEvent.change(select, {target: {value: '102'}})
    })
    screen.debug();
    expect(screen.findByText(/xxx/)).toBeInTheDocument();
  })

https://zenn.dev/bom_shibuya/articles/5c3ae7745c5e94

bz0bz0

ReactDOMなら下記のようにクエリセレクタ使えるのか。

import ReactDOM from 'react-dom';
ReactDOM.render(<Counter />, container);
const button = container.querySelector('button');
bz0bz0
warning: Each child in a list should have a unique "key" prop.

ユニークなキーが必要とのことなので、mapメソッドのコールバック関数の第二引数(index)をキーとして付与することで解決できる。(key={index}が必要)ReactではどのDOMが変更されたのかが分かる必要がありmapメソッドのindex番号を付与し識別子を与えることが必要となります。
https://techtechmedia.com/unique-key-prop-react/

                {genreResult.data && genreResult.data.map((genre:Genre, index) => (
                  <option value={genre.genre_id} key={index}>{genre.genre_name}</option>
                ))}
bz0bz0

debugで出力する内容が途中で切れるとき

    const maxLengthToPrint= 100000
    screen.debug(undefined, maxLengthToPrint)

https://www.fixes.pub/program/231057.html

jestにおいてテストを入れるフォルダの名前は__tests__にする必要があるのか

  • デフォルトでJestは__tests__ディレクトリ以下のテストプログラムを実行
  • __tests__以外にも予約されているフォルダ名がいくつかありますが、すべて__で囲われている
    • 「__snapshots__」 , 「__mocks__」
  • 特にルールとして明示的に定まっているわけではありませんが、慣習として「内部的に使用する」場合にアンダーバーを付けることが多い

https://teratail.com/questions/284493?utm_source=pocket_mylist

bz0bz0
const spiedSetStatus = jest.spyOn(analyticsModule, "setStatus")
  • analyticsModule といった名前空間を定義しインポートしたモジュールが提供する setStatus という関数に対してスパイをする
  • 実際の関数を監視してどういった引数で呼ばれたか何回よばれたかをなどを格納するインスタンスを返す

hooksをを呼び出すためのヘルパー関数

  renderHook(() => useInitialAnalytics({ id: "foo", role: "bar" }))
  expect(spiedSetStatus).toBeCalledWith({ id: "foo", role: "bar" })
  expect(spiedSendPageview).toBeCalled()

Matcher

  • toBe は === を使用して厳密な等価性をテスト
bz0bz0

https://qiita.com/schroneko/items/18041ca5f2917077e320

$ yarn run lint
yarn run v1.22.5
$ next lint
info  - Loaded env from /home/kz/wk/narou-front/.env.local
info  - Loaded env from /home/kz/wk/narou-front/.env.production
info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
Error: Cannot read config file: /home/kz/wk/narou-front/node_modules/eslint-config-prettier/@typescript-eslint.js
Error: "prettier/@typescript-eslint" has been merged into "prettier" in eslint-config-prettier 8.0.0. See: https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md#version-800-2021-02-21
Referenced from: /home/kz/wk/narou-front/.eslintrc.js
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.