【react】queryParameterをテストする
はじめに
urlにqueryParameterを付与するCustom hookのテストを行いたかったのですが、その際に少し工夫が必要だったので備忘録としてまとめます
環境
- react 18.2.0
- react-router-dom 6.15.0
- vite 4.4.5
- vitest 0.34.1
- happy-dom 10.11.0
ベースとなる諸々
テスト用にinputに入力した値をqueryParameterにセットする簡素な仕組みを作成していきたいと思います
CRA
viteで基盤となるプロジェクトを作成していきます
$ npm create vite@latest
✔ Project name: … test-query-parameters
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Scaffolding project in /mnt/d/project/blog-code/test-query-parameters...
Done. Now run:
cd test-query-parameters
npm install
npm run dev
ディレクトリを移動してnpm install
も行います
余談ですが
nodeの管理にvoltaを使用しているのですが、package.jsonにpinしておくと、他の方はnpm install
するだけで
package.jsonにpinされたnodeとnpmのバージョンが適用されるので便利ですです
$ volta pin node
$ volta pin npm
{
...
"volta": {
"node": "18.17.1",
"npm": "9.8.1"
}
}
ライブラリインストール
queryParameterのテストを行うのに必要なライブラリを入れていきます
コンポーネントを含めたテストを行うため、happy-domもここで入れておきます
$ npm install react-router-dom
$ npm install -D vitest
$ npm install -D happy-dom
$ npm install -D @testing-library/react
実装
routerを定義します
簡素な実装となるため、特に分けたりはせずにべたで書いていきます
import { useRoutes } from "react-router-dom"
import App from "./App"
export const Routes = () => {
const element = useRoutes([
{
path: '/',
element: <App />
},
])
return <>{element}</>
}
main.tsxに上記Routesを適用します
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<Routes />
</BrowserRouter>
</React.StrictMode>,
)
queryParameterに値をセットするCustom hookを作成します
セットにはreact-router-domから提供されているuseSearchParamsを用います
import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
export const useQueryParameter = () => {
const [searchParams, setSearchParams] = useSearchParams();
const testValue = searchParams.get('test') ?? '';
const handleOnChange = useCallback(
(value: string) => {
// 本来はここで値変換等を行った際のテストをしてます
setSearchParams((prev) => ({
...Object.fromEntries(prev),
test: value,
}));
},
[setSearchParams]
);
return { testValue, handleOnChange };
};
最後にAppにinputを適当に設置して、上記hookから受け取った値を使用してvalueとonChangeを定義します
function App() {
const [count, setCount] = useState(0)
const { testValue, handleOnChange } = useQueryParameter()
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<input
type='text'
id='test'
name='test'
value={testValue}
onChange={(e) => handleOnChange(e.target.value)}
/>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
これでinputに入力した値がqueryParameterとしてurlに含まれるようになりました。確認してみましょ
いい感じですねー
最終のディレクトリ構成はこんな感じです
├── public
│ └── vite.svg
├── src
│ ├── assets
│ │ └── react.svg
│ ├── App.css
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ ├── router.tsx
│ ├── useQueryParameter.ts
│ └── vite-env.d.ts
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
ここからhookのuseQueryParameterに対して、testを書いていきたいと思います
test実装
まずはテストをするためにvite.configとtsconfigの設定を変えていきます
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'happy-dom'
}
})
"compilerOptions": {
"types": [
"vitest/globals"
]
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test": "vitest"
},
ここからhookのテストを書いていきます
ダメな例
今回react-router-domのuseSearchParamsを用いているので、Routerでwrapしてあげないと怒られてしまいます
describe('useQueryParameterのテスト', () => {
test('testのクエリパラメータが存在しない場合は空文字', () => {
const { result } = renderHook(() => useQueryParameter());
expect(result.current.testValue).toBe('');
});
});
❯ src/useQueryParameter.test.tsx (1)
❯ useQueryParameterのテスト (1)
× testのクエリパラメータが存在しない場合は空文字
FAIL src/useQueryParameter.test.tsx > useQueryParameterのテスト > testのクエリパラメータが存在しない場合は空文字
Error: useLocation() may be used only in the context of a <Router> component.
renderHookの際にwrapするには、第二引数のoptionsにWrapperコンポーネントを指定してあげることで実現できます
また、react-router-domにはテストに最適なMemoryRouterというコンポーネントがあるためこちらを使ってwrapしてあげます
良い例
上記2つを踏まえてよい例の最終的なテストコードは以下となります
import { renderHook, act } from '@testing-library/react';
import { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { useQueryParameter } from './useQueryParameter';
const Wrapper = ({
children,
params,
}: {
children: ReactNode;
params?: string;
}) => (
<MemoryRouter initialEntries={params ? [`/?${params}`] : undefined}>
{children}
</MemoryRouter>
);
describe('useQueryParameterのテスト', () => {
test('testのクエリパラメータが存在しない場合は空文字', () => {
const { result } = renderHook(() => useQueryParameter(), {
wrapper: ({ children }) => <Wrapper>{children}</Wrapper>,
});
expect(result.current.testValue).toBe('');
});
test('testのクエリパラメータが存在する場合、testValueが同値となっている', () => {
const { result } = renderHook(() => useQueryParameter(), {
wrapper: ({ children }) => (
<Wrapper params='test=value'>{children}</Wrapper>
),
});
expect(result.current.testValue).toBe('value');
});
test('inputの値を変更した際、クエリパラメータに反映されている', () => {
const { result } = renderHook(() => useQueryParameter(), {
wrapper: ({ children }) => (
<Wrapper params='test=value'>{children}</Wrapper>
),
});
expect(result.current.testValue).toBe('value');
act(() => {
result.current.handleOnChange('お酒飲みたい');
});
expect(result.current.testValue).toBe('お酒飲みたい');
});
});
renderHookのwrapperに、今回用に定義したWrapperを渡してあげます
parameterの初期値を変更できるようにpropsで受け取れるようにします
ちなみに、ドキュメントに
NOTE: When using renderHook in conjunction with the wrapper and initialProps options, the initialProps are not passed to the wrapper component.
と書かれているように2つ同時には使えないようです
上記テストを実行してみます
✓ src/useQueryParameter.test.tsx (3)
✓ useQueryParameterのテスト (3)
✓ testのクエリパラメータが存在しない場合は空文字
✓ testのクエリパラメータが存在する場合、testValueが同値となっている
✓ inputの値を変更した際、クエリパラメータに反映されている
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 00:15:37
Duration 2.70s (transform 104ms, setup 0ms, collect 432ms, tests 43ms, environment 293ms, prepare 226ms)
うまく通りましたねー良い感じってやつです
おわりに
renderHook時に今回ケースのように、何かしらのコンポーネント配下でテストを行いたいケースはわりかしありそうだなぁと思いました
ソースコードはgithubにあげてありますー
Discussion