h-yoshikawa44 / resas-graph-app

課題
https://resas-graph-app.vercel.app
0 stars 0 forks source link

テストコードの作成 #11

Closed h-yoshikawa44 closed 2 years ago

h-yoshikawa44 commented 2 years ago

作業内容

h-yoshikawa44 commented 2 years ago

書き方調査

正直、フロントエンドのテストに関する知見がほとんどないため調べる所から。

この辺りを参考にしてみようと思う。

テストしたい要素を特定する方法で悩んでいたが、data-testId という data 属性を使えばいいらしい。 本番ビルドでは削除するような Babel プラグイン(babel-plugin-react-remove-properties)もあるとのこと。

emotion でのスタイルをテストしたい場合は、@emotion/jest を使えばいいとのこと。

h-yoshikawa44 commented 2 years ago

Jest 実行時に ky のインポートがエラーになる

domain のテストをやろうとしたところ、ky のインポートのところでエラーになってしまった。 ky は ESM であることの影響っぽい。

  ● 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:

    C:\Users\ballk\development\coding-test\resas-graph-app\node_modules\ky\distribution\index.js:2
    import { Ky } from './core/Ky.js';
    ^^^^^^

    SyntaxError: Cannot use import statement outside a module

    > 1 | import ky, { Options } from 'ky';
        | ^
      2 | import { DEFAULT_API_OPTIONS } from '@/config/ky';
      3 | import {
      4 |   PopulationCategories,

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1728:14)
      at Object.<anonymous> (src/domains/getPopulations/getPopulations.ts:1:1)

試したこと

Jest の設定に以下を追加

moduleNameMapper: {
  '^ky$': require.resolve('ky').replace('index.js', 'umd.js'),
}

↓ 違うエラーに

    Configuration error:

    Could not locate module ky mapped as:
    C:\Users\ballk\development\coding-test\resas-graph-app\node_modules\ky\distribution\umd.js.

    Please check your configuration for these entries:
    {
      "moduleNameMapper": {
        "/^ky$/": "C:\Users\ballk\development\coding-test\resas-graph-app\node_modules\ky\distribution\umd.js"
      },
      "resolver": undefined
    }

    > 1 | import ky, { Options } from 'ky';
        | ^
      2 | import { DEFAULT_API_OPTIONS } from '@/config/ky';
      3 | import {
      4 |   PopulationCategories,

      at createNoMappedModuleFoundError (node_modules/jest-resolve/build/resolver.js:579:17)
      at Object.<anonymous> (src/domains/getPopulations/getPopulations.ts:1:1)

Jest 設定に以下を追加

moduleNameMapper: {
  "ky": "ky/umd"
}

↓ 違うエラーに

    Configuration error:

    Could not locate module ky mapped as:
    ky/umd.

    Please check your configuration for these entries:
    {
      "moduleNameMapper": {
        "/ky/": "ky/umd"
      },
      "resolver": undefined
    }

    > 1 | import ky, { Options } from 'ky';
        | ^
      2 | import { DEFAULT_API_OPTIONS } from '@/config/ky';
      3 | import {
      4 |   PopulationCategories,

      at createNoMappedModuleFoundError (node_modules/jest-resolve/build/resolver.js:579:17)
      at Object.<anonymous> (src/domains/getPopulations/getPopulations.ts:1:1)

Jest 設定に以下を追加

transformIgnorePatterns: [
    '/node_modules/(?!(ky))'
  ]

↓ 元々と同じエラー ` '/node_modules/(?!(ky/))'でも一緒


こちらの記事がだいぶ近い状態だったので、試してみた

...がエラーが出る。

No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In C:\Users\ballk\development\coding-test\resas-graph-app
  82 files checked.
  testMatch: **/*.test.ts, **/*.test.tsx - 7 matches
  testPathIgnorePatterns: \\node_modules\\ - 82 matches
  testRegex:  - 0 matches
Pattern: NODE_OPTIONS=--experimental-vm-modules - 0 matches
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

↓ そうこうしてるうちに動くようになった。

結局やったこととしては、 1..babelrcbabel.config.js に名称を変えた(babel-jest は.babelrcを設定ファイルとして読めないらしい)

module.exports = {
  presets: ['next/babel', '@emotion/babel-preset-css-prop'],
  env: {
    production: {
      plugins: ['react-remove-properties'],
    },
  },
};

Jest 設定に以下を追記

transformIgnorePatterns: ['/node_modules/(?!ky)/'],
h-yoshikawa44 commented 2 years ago

ky の globalThis.Headers が入らない

正常系は何とか動作したが、異常系のテストを書こうとすると、こんなエラーになった。

    TypeError: globalThis.Headers is not a constructor

      19 |     ...options,
      20 |   };
    > 21 |   const response = await ky.get(
         |                             ^
      22 |     'population/composition/perYear',
      23 |     mergedOptions
      24 |   );

      at mergeHeaders (node_modules/ky/source/utils/merge.ts:15:17)
      at new Ky (node_modules/ky/source/core/Ky.ts:102:13)
      at Function.create (node_modules/ky/source/core/Ky.ts:15:14)
      at Function.get (node_modules/ky/source/index.ts:16:56)
      at getPopulations (src/domains/getPopulations/getPopulations.ts:21:29)
      at Object.<anonymous> (src/domains/getPopulations/getPopulations.test.ts:39:18)

そもそも ky がブラウザ用のライブラリなので、Node.js として使用する場合は ky-universal の方を使うと両方いけるとのこと。

ただ、ky-universal に切り替えると、今度はまた構文の問題が発生した。 トップレベル await を使ってるからダメっぽい? Jest 設定の transformIgnorePatterns に ky-universal を追加しても発生するので、よくわからない...

    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:

    C:\Users\ballk\development\coding-test\resas-graph-app\node_modules\ky-universal\index.js:52
        globalThis.ReadableStream = await Promise.resolve().then(() => _interopRequireWildcard(require('web-streams-polyfill/ponyfill/es2018')));
                                    ^^^^^

    SyntaxError: await is only valid in async function

    > 1 | import { HTTPError } from 'ky-universal';
        | ^
      2 | import { rest } from 'msw';
      3 | import { setupServer } from 'msw/node';
      4 | import getPopulations from './getPopulations';

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1728:14)
      at Object.<anonymous> (src/domains/getPopulations/getPopulations.test.ts:1:1)
h-yoshikawa44 commented 2 years ago

テストが中途半端で終わってしまうのが気がかりではあるが、提出期限になったので、できた分だけで一旦プルリクを出して、提出しようと思う。

h-yoshikawa44 commented 2 years ago

globalThis の中身を手動で設定してあげる

根強く調査をしていたところ、ほぼ同じ問題に遭遇して回避策を書かれていた Issue を発見した。

jest.setup.js に以下を追加して globalThis を手動で設定

const fetch = require('node-fetch');

globalThis.fetch = fetch;
globalThis.Request = fetch.Request;
globalThis.Headers = fetch.Headers;
globalThis.Response = fetch.Response;

参考 Issue では ky-universal の回避策として載っていたが、自分の場合はこれを追加してもトップレベル await でエラーになることは変わらなかった。 ただ、ky でやってみたところ問題なくテストが動作するようになった。 前コメントで「ky の globalThis.Headers が入らない」と書いていたが、結局は globalThis を自分で用意してあげればよかったということらしい。

それと jest-environment は jsdom の必要があるっぽい。 node でやろうとすると、afterResponse のところで Blob is not defined と ReferenceError になってしまう。

h-yoshikawa44 commented 2 years ago

Blob について

ky の afterResponse でレスポンスの詰め替えをする際、new Blobをしていたが、Node.js では Blob が使えない?ようだったので、その辺りでデータがおかしくなり、response.json() 時に JSON に変換できないというエラーになってしまった。 Node.js でも Blob を使う方法はいくつかあり、いろいろ試したが、どれもうまくいかず...。

node-fetch の Blob:ブラウザ側の Blob と挙動が違う? fetch-blob:Blob 自体にはなるが、中身がない Blob になってしまう buffer からインポートした Blob:中身のある Blob になるが、response.json() で取り出すときには Buffer になってしまい、変換できない

そもそも Node.js でバイナリデータを扱う時は Buffer を使うもの ということで、Blob でどうにかするのは諦めて Buffer でレスポンスの詰め替えをすることにした。 (通常時とテスト時で Blob と Buffer を切り替えようかと思ったが、ブラウザ側でも問題なく動作したのでこうした)

const body = Buffer.from(JSON.stringify(data, null, 2), 'utf8');
return new Response(body, init);
h-yoshikawa44 commented 2 years ago

msw の書き方について

明らかに失敗するはずのテストがパスしていたので、何かおかしいと思い改めて書き方を調べたら、ちょっと間違えていたことがわかった。 また、ファイル構成をきっちり分けている記事を見かけたので、それを取り入れてみた。

↓ ちゃんと動作するようになった。

h-yoshikawa44 commented 2 years ago

カスタムフックのテスト

@testing-library/react-hooks で書いていく。 renderHookを使うことで、テスト内でもカスタムフックを呼び出すことができる。 state の更新処理を行う関数や、useEffect の中で state 更新処理を実行している場合は、act で囲う必要がある。 (usePrefecture では後者)

test('state: initial', async () => {
    await act(async () => {
      const { result, waitFor } = renderHook(() => usePrefecture());
      expect(result.current.prefectures).toBeUndefined();
      expect(result.current.isLoading).toBe(false);
      expect(result.current.errorMessage).toBe('');

      await waitFor(() => result.current.isLoading === true);
      await waitFor(() => result.current.isLoading === false);
    });
  });

注意点として、state 更新処理は非同期で進んでいくため、ただ act で囲うだけでは、act 外に出た時に useEffect 内の state 更新処理が実行される形になり act で囲ってないと警告が出る。

    Warning: An update to TestComponent inside a test was not wrapped in act(...).

    When testing, code that causes React state updates should be wrapped into act(...):

    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */

そのため、waitFor で確実に最後の state 更新処理が終わるまで待つようにする必要がある。 usePrefecture では finally ブロックで isLoading を更新しているので、それを waitFor で見るようにした。

h-yoshikawa44 commented 2 years ago

page のテスト

どうテストを書いていくか悩んでいたが、カスタムフックのテストはすでにやっているので、カスタムフック部分と使用しているコンポーネントをモックにして実施。

カスタムフックのモックはテストごとに返り値を変更したかったので spyOn でモック作成。 注意点として、デフォルトエクスポートの場合は、一旦名前付きインポートにしたうえで default で使うようにする必要があった。

import * as usePrefecture from '@/hooks/usePrefecture';
.
.
.
const response: ReturnType<typeof usePrefecture.default> = {
    prefectures: undefined,
    isLoading: true,
    errorMessage: '',
  };
jest.spyOn(usePrefecture, 'default').mockReturnValue(response);
h-yoshikawa44 commented 2 years ago

カバレッジ

一旦、これくらいにはなった。 まだ改善の余地はありそうではあるが、とりあえずはここで終わりにする。

------------------------------------------------|---------|----------|---------|---------|-------------------
File                                            | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------------------------------|---------|----------|---------|---------|-------------------
All files                                       |   98.01 |     87.5 |     100 |      98 |
 components/common/Alert                        |     100 |    88.88 |     100 |     100 |
  Alert.tsx                                     |     100 |    88.88 |     100 |     100 | 9
 components/common/CheckBox                     |     100 |      100 |     100 |     100 |
  CheckBox.tsx                                  |     100 |      100 |     100 |     100 |
 components/common/Header                       |     100 |      100 |     100 |     100 |
  Header.tsx                                    |     100 |      100 |     100 |     100 |
  index.ts                                      |       0 |        0 |       0 |       0 |
 components/common/Toast                        |    87.5 |    66.66 |     100 |    87.5 |
  Toast.tsx                                     |    87.5 |    66.66 |     100 |    87.5 | 25
 components/model/Population/PopulationGraph    |     100 |       50 |     100 |     100 |
  PopulationGraph.tsx                           |     100 |       50 |     100 |     100 | 13
 components/model/Prefecture/PrefectureFieldset |     100 |      100 |     100 |     100 |
  PrefectureFieldset.tsx                        |     100 |      100 |     100 |     100 |
 components/page/Home                           |     100 |      100 |     100 |     100 |
  Home.tsx                                      |     100 |      100 |     100 |     100 |
 config                                         |     100 |       90 |     100 |     100 |
  ky.ts                                         |     100 |       90 |     100 |     100 | 17
 domains/getPopulations                         |     100 |      100 |     100 |     100 |
  getPopulations.ts                             |     100 |      100 |     100 |     100 |
  index.ts                                      |       0 |        0 |       0 |       0 |
 domains/getPrefectures                         |     100 |      100 |     100 |     100 |
  getPrefectures.ts                             |     100 |      100 |     100 |     100 |
  index.ts                                      |       0 |        0 |       0 |       0 |
 hooks/usePopulation                            |   94.28 |    81.81 |     100 |   94.11 |
  index.ts                                      |       0 |        0 |       0 |       0 |
  usePopulation.ts                              |   94.28 |    81.81 |     100 |   94.11 | 32,70
 hooks/usePrefecture                            |   94.11 |       75 |     100 |   94.11 |
  index.ts                                      |       0 |        0 |       0 |       0 |
  usePrefecture.ts                              |   94.11 |       75 |     100 |   94.11 | 29
 mock                                           |     100 |      100 |     100 |     100 |
  constants.ts                                  |     100 |      100 |     100 |     100 |
  handler.ts                                    |     100 |      100 |     100 |     100 |
  server.ts                                     |     100 |      100 |     100 |     100 |
 mock/api                                       |     100 |    91.66 |     100 |     100 |
  resas.ts                                      |     100 |    91.66 |     100 |     100 | 65-66
 mock/data                                      |     100 |      100 |     100 |     100 |
  errorResponse.ts                              |     100 |      100 |     100 |     100 |
  population.ts                                 |     100 |      100 |     100 |     100 |
  prefecture.ts                                 |     100 |      100 |     100 |     100 |
 models                                         |     100 |       92 |     100 |     100 |
  ErrorResponse.ts                              |     100 |    88.88 |     100 |     100 | 19
  Population.ts                                 |     100 |     90.9 |     100 |     100 | 40
  Prefecture.ts                                 |     100 |      100 |     100 |     100 |
 styles                                         |     100 |      100 |     100 |     100 |
  constants.ts                                  |     100 |      100 |     100 |     100 |
------------------------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 11 passed, 11 total
Tests:       44 passed, 44 total
Snapshots:   0 total
Time:        11.811 s
Ran all test suites.
Done in 13.09s.