以前、 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