以前、 MUI DataGrid の Custom Operator を作りました。
React + MUI のDataGridにて、ある列が複数の日付を持つデータに対し、valueFormatter・sortComparator・filterModelを使って表示・ソート・フィルタしてみた - メモ的な思考的な
この時に作成した Custom Operator の getApplyFilterFn に対して Jest でテストコードを書こうと思ったところ、いろいろ悩んだためメモを残します。
目次
環境
- React.js 17.0.2
- React Router 6.0.1
- @mui/x-data-grid 5.0.1
- date-fns 2.26.0
- TypeScript 4.5.2
- Vite.js 2.6.14
- Jest 27.3.1
- jest-mock-extended 2.0.4
なお、以前の記事後、 @mui/x-data-grid が正式バージョンの 5.0.1 になったため、バージョンアップをしておきます。
また、以前のコードで date-fns の import が
import isSameDay from 'date-fns/isSameDay'
となっている部分があったため、今回作成するテストコードを実行すると
TypeError: (0 , isSameDay_1.default) is not a function
というエラーになってしまいます。
stackoverflowなどによると
- import方法を変える
import {isSameDay} from 'date-fns'
- tsconfig.json を
"esModuleInterop": falseから"esModuleInterop": trueへ変更する
のどちらかを行えば良いようです。
- javascript - TypeError: format is not a function in date-fns - Stack Overflow
- esModuleInterop オプションの必要性について - Qiita
そこで今回は前者の import 方法を変更しておきます。
Jest + ts-jest のセットアップ
前回のコードには Jest をインストールしていなかったため、追加します。
Jest まわりをインストールします。
なお、Transformer には色々ありますが、今回は ts-jest を使用します。
# インストール % yarn add -D jest @types/jest ts-jest # 初期設定 % jest --init The following questions will help Jest to create a suitable configuration for your project ✔ Would you like to use Jest when running "test" script in "package.json"? … yes ✔ Would you like to use Typescript for the configuration file? … no ✔ Choose the test environment that will be used for testing › node ✔ Do you want Jest to add coverage reports? … yes ✔ Which provider should be used to instrument code for coverage? › v8 ✔ Automatically clear mock calls and instances between every test? … no ✏️ Modified path/to/react_mui_with_vite/package.json 📝 Configuration file created at path/to/react_mui_with_vite/jest.config.js
生成された jest.config.js に以下を追記します。
module.exports = { moduleNameMapper: { '^@/(.+)': '<rootDir>/src/$1' }, roots: ['<rootDir>/src'], testPathIgnorePatterns: ['/node_modules/'], transform: { '^.+\\.(ts|tsx)$': 'ts-jest' } }
テスト方法を考える
getApplyFilterFn は
getApplyFilterFn: (filterItem: GridFilterItem) => { if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { return null } return (params: GridCellParams): boolean => { if (!params.value || !Array.isArray(params.value)) { return false } return params.value.filter((v) => isSameDay(v, toFilterValue(filterItem))).length > 0 } },
と定義していました。
そのため、テストコードは
GridFilterItem型の引数を渡してgetApplyFilterFn()を呼ぶ- 関数が戻ってくるので、
GridCellParams型の引数を渡してその関数を呼ぶ - 戻り値の boolean を検証する
とすれば良さそうでした。
型定義を見てみると、 GridFilterItem にはプロパティが4つありました。
https://github.com/mui-org/material-ui-x/blob/v5.0.1/packages/grid/modules/grid/models/gridFilterItem.ts
一方、 GridCellParams にはプロパティや関数が数多くありました。テストで使うものは GridCellParams.value だけですが、 interface の制約に従うと他にも実装が必要そうでした。
https://github.com/mui-org/material-ui-x/blob/v5.0.1/packages/grid/modules/grid/models/params/gridCellParams.ts
そこで今回は、モックによるテストコードを考えることにしました。
ダメだったモックの方法
Jest では、標準のモックがいくつも用意されています。
まずはモック関数 jest.fn() を使ってみることにしました。
jest.fn() で value だけ値を返すように
const m = jest.fn<GridCellParams, []>().mockImplementation(() => { return { value: [new Date(2021, 10, 27)] } })
と使ってみたところ
TS2345: Argument of type '() => { value: Date; }' is not assignable to parameter of type '() => GridCellParams<any, any, any>'. Type '{ value: Date; }' is missing the following properties from type 'GridCellParams<any, any, any>': id, field, formattedValue, row, and 6 more.
とエラーになりました。 value 以外にも id などの実装が必要そうでした。
続いて jest.spyOn() を
const n = jest.spyOn(GridCellParams, 'value').mockReturnValue('')
としたところ、
TS2693: 'GridCellParams' only refers to a type, but is being used as a value here.
というエラーになりました。
公式ドキュメントによると、第1引数は object なので interface は渡せないようです。
https://jestjs.io/ja/docs/jest-object#jestspyonobject-methodname
他に Jest の標準で使えそうなものを探しましたが、見当たりませんでした。
もし使えそうなものをご存じの方がいれば、教えていただけるとありがたいです。
jest-mock-extended でモックを実装すればOK
Jest で interface をモックする方法を調べたところ、以下の stackoverflow に出会いました。
mocking - Mock a typescript interface with jest - Stack Overflow
そこでは jest-mock-extended が紹介されていました。
https://github.com/marchaos/jest-mock-extended
READMEを読んだところ、 interface をモックしている例が記載されていため、試してみることにしました。
まずは jest-mock-extended をインストールします。
% yarn add -D jest-mock-extended
続いてテストコードを書いていきます。
まずは GridCellParams のモックを作成します。value が値を返すように定義し、他は特に定義しません。
const cellParams = mock<GridCellParams>() cellParams.value = [new Date(2021, 10, 27), new Date(2021, 11, 28), new Date(2021, 11, 29)]
次に GridFilterItem のモックも作成します。 getApplyFilterFn の中で値をチェックしているプロパティについては、適当な値をセットします。
なお、フィルター用の値 value には '2021-11-27' を指定しておきます。
const filterItem = mock<GridFilterItem>() filterItem.columnField = 'a' filterItem.value = '2021-11-27' filterItem.operatorValue = 'is'
あとは検証です。
まずは getApplyFilterFn() を実行します。
const fn = isOperator.getApplyFilterFn(filterItem)
getApplyFilterFn() の戻り値は null の可能性もありますが、このテストケースでは null にはならないはずです。
そのため、
expect(fn).not.toBeNull()
という検証を追加します。
次に、戻り値の関数 fn を実行しますが、
fn(cellParams)
とすると
TS2721: Cannot invoke an object which is possibly 'null'.
になってしまいます。
そこで、オプショナルチェーンを使うことでエラーとならないようにします。
オプショナルチェーン (?.) - JavaScript | MDN
const actual = fn?.(cellParams)
あとは検証して終わりです。
expect(actual).toBe(true)
テストコードの全体像は以下のとおりです。
念のため、 false となる条件も検証しています。
import {mock} from 'jest-mock-extended' import {GridCellParams, GridFilterItem} from '@mui/x-data-grid' import {isOperator} from '@/components/functions/datagrid/operators' describe('is operator test', () => { const cellParams = mock<GridCellParams>() cellParams.value = [new Date(2021, 10, 27), new Date(2021, 11, 28), new Date(2021, 11, 29)] describe('フィルタの値がセルの値と一致する場合', () => { const filterItem = mock<GridFilterItem>() filterItem.columnField = 'a' filterItem.value = '2021-11-27' filterItem.operatorValue = 'is' it('trueを返す', () => { const fn = isOperator.getApplyFilterFn(filterItem) // nullの可能性があるため、 // ・nullではないことを確認 // ・オプショナルチェーン (?.) で関数を実行 // とする expect(fn).not.toBeNull() const actual = fn?.(cellParams) expect(actual).toBe(true) }) }) describe('フィルタの値がセルの値と一致しない場合', () => { const filterItem = mock<GridFilterItem>() filterItem.columnField = 'a' filterItem.value = '2021-11-30' filterItem.operatorValue = 'is' it('falseを返す', () => { const fn = isOperator.getApplyFilterFn(filterItem) expect(fn).not.toBeNull() const actual = fn?.(cellParams) expect(actual).toBe(false) }) }) })
テストを実行するとパスしました。
% yarn test
yarn run v1.22.11
$ jest
PASS src/tests/components/functions/datagrid/operators.test.ts (5.997 s)
is operator test
フィルタの値がセルの値と一致する場合
✓ trueを返す (4 ms)
フィルタの値がセルの値と一致しない場合
✓ falseを返す
...
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
ソースコード
Github に上げました。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample
今回のPRはこちらです。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample/pull/2