検索条件
お知らせ
現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
条件分岐でコンポーネントの出し分けをしている時に正しくコンポーネントが出ているかどうかに使えるやつ
UIロジックのリグレッションテストで使える
分岐結果の出力を見てるだけなのでテストとして壊れづらく、運用しやすいと考えている
Next.jsで確認してるけど素のReactでも同じだと思う
Env |
Ver |
@swc/core |
1.2.133 |
@swc/jest |
0.2.17 |
jest |
27.4.7 |
next |
12.0.8 |
react |
17.0.2 |
react-dom |
17.0.2 |
react-test-renderer |
17.0.2 |
typescript |
4.5.4 |
テストのためにコンポーネントを細かくexport
すると名前空間が汚染されるのが悩み…
type BaseProps = {
id: string;
};
type SwitchExampleProps = BaseProps & {
display: 'Foo' | 'Bar';
};
export const Foo = (props: BaseProps) => {
return (
<div id={props.id}>
<p>Foo</p>
</div>
);
};
export const Bar = (props: BaseProps) => {
return (
<div id={props.id}>
<p>Bar</p>
</div>
);
};
export const SwitchExample = (props: SwitchExampleProps) => {
if (props.display === 'Foo') {
return <Foo id={props.id} />;
} else {
return <Bar id={props.id} />;
}
};
react-testing-libraryの.toHaveAttribute()
や.toHaveDisplayValue()
を書き連ねるより圧倒的に楽で保守性も良いと思う
import TestRenderer from 'react-test-renderer';
import { Bar, Foo, SwitchExample } from './SwitchExample';
type TestCase = {
name: string;
param: Parameters<typeof SwitchExample>[0];
actual: JSX.Element;
};
describe('SwitchExample', () => {
const testCaseItems: TestCase[] = [
{
name: 'Foo',
param: {
id: 'hoge',
display: 'Foo',
},
actual: <Foo id={'hoge'} />,
},
{
name: 'Bar',
param: {
id: 'piyo',
display: 'Bar',
},
actual: <Bar id={'piyo'} />,
},
];
testCaseItems.forEach((item) => {
// eslint-disable-next-line jest/valid-title
it(`switched condition ${item.name}`, () => {
const result = TestRenderer.create(
<SwitchExample id={item.param.id} display={item.param.display} />
);
const actual = TestRenderer.create(<>{item.actual}</>);
expect(result.toJSON()).toStrictEqual(actual.toJSON());
});
});
});
Env |
Ver |
next |
11.1.2 |
typescript |
4.3.4 |
export const useRouter = () => {
return {
route: '/',
pathname: '',
query: '',
asPath: '',
push: jest.fn(),
replace: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => null),
};
};
global.navigator
をモックにする方法
jest
のモック機能はプロパティのモックが出来ないので、実オブジェクトを強制的に書き換えて実施する
Object.defineProperty()
を利用して実装
value
, プロパティが返す値
configurable
, 再定義可能かどうか、設定しないと再実行でコケる
- 実際の使用ではユーティリティ関数を作っておき、
afterAll()
でnavigator.userAgent
を初期値に戻すのが望ましい
Object.defineProperty(global.navigator, 'userAgent', {
value:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
configurable: true,
});
- 理論上可能そうだったので、ひたすら試行錯誤してたらできたのでメモがてら
axios
そのものをモックして、その結果に応じた処理が想定通り走っているかどうかを確認したい
create-react-app
で作成したTypeScript Reactのプロジェクトにnpm t
を掛けてテストすることを想定
- 但し検証用のプロジェクトは@lycolia/ts-boilerplate-generator-cliで作成
Env |
Ver |
react |
17.0.1 |
react-scripts |
4.0.2 |
typescript |
4.1.3 |
- ページ読み込み時に一回だけAPIを蹴ってなんかすることを想定
import axios from 'axios';
import { useEffect } from 'react';
export const AsyncHookExample = () => {
useEffect(() => {
axios
.get('https://localhost/')
.then((res) => console.log(res))
.catch((err) => console.error(err));
}, []);
return <p />;
};
axios
をモックした上で、モック関数のPromise
の解決を待ち、 Promise
のコールバックをspyOn
して検査する内容
- モック関数の
Promise
の解決法を求めるのにハマりましたが、これはfoo.mock.results[0].value
に対してawait expect(foo.mock.results[0].value).resolves
としてやればいける
- 細かいことはソースコードのコメント参照
import { render } from '@testing-library/react';
import axios from 'axios';
import { AsyncHookExample } from './AsyncHookExample';
// `axios` をモックにする
jest.mock('axios');
// `axios` のモックを取得
const mockedAxios = axios as jest.Mocked<typeof axios>;
// `console.log()` を `spyOn'
const spiedConsoleLog = jest.spyOn(console, 'log');
// `console.error()` を `spyOn'
const spiedConsoleError = jest.spyOn(console, 'error');
describe('AsyncHookExample', () => {
it('resolve promise in Hook', async () => {
// テストに期待する結果
// `axios` はモックなので値は適当
const beResult = {
status: 200,
data: null,
};
// `axios` のモックが `resolve` する値を設定
mockedAxios.get.mockResolvedValue(beResult);
// コンポーネントを `render` して `useEffect` を走らせる
render(<AsyncHookExample />);
// `axios.get()` が呼ばれたことを確認
expect(mockedAxios.get).toBeCalled();
// モックの結果を取得
const testResult = mockedAxios.get.mock.results[0].value;
// `reject` された値が期待通りであることを確認
await expect(testResult).resolves.toEqual(beResult);
// `useEffect` の中の `Promise` の中にある `console.log()` が呼ばれたことを確認
expect(spiedConsoleLog).toBeCalled();
});
it('reject promise in Hook', async () => {
// テストに期待する結果
// `axios` はモックなので値は適当
const beResult = {
status: 400,
data: null,
};
// `axios` のモックが `reject` する値を設定
mockedAxios.get.mockRejectedValue(beResult);
// コンポーネントを `render` して `useEffect` を走らせる
render(<AsyncHookExample />);
// `axios.get()` が呼ばれたことを確認
expect(mockedAxios.get).toBeCalled();
// モックの結果を取得
const testResult = mockedAxios.get.mock.results[0].value;
// `reject` された値が期待通りであることを確認
await expect(testResult).rejects.toEqual(beResult);
// `useEffect` の中の `Promise` の中にある `console.error()` が呼ばれたことを確認
expect(spiedConsoleError).toBeCalled();
});
});
mockResolvedValue
のスコープやらモックのPromise
を解決させるマッチャやら調べるのに手間取った
- あとはそもそも
Promise
が解決した結果がどこに入るのかとか、兎に角あれこれ
expect(mockedAxios.get).toBeCalled()
が通るのはすぐに気づいたので、なら出来るだろうとひたすら調べてた
- まず
history.push('/path/to/dest')
自体のテストを書くことはないはずなので、何かしらの副作用として実行される事が前提です
- testing-library.com に書いてあることの応用です
/**
* useHistory() を使えるようにするテストユーティリティ
*/
const renderWithRouter = (children: JSX.Element, { route = '/' } = {}) => {
window.history.pushState({}, 'Test page', route);
return render(children, {
wrapper: BrowserRouter,
});
};
// spy 用のモック関数
const mockPush = jest.fn();
// react-router-dom をモックにする
jest.mock('react-router-dom', () => ({
useHistory: () => ({
// モック関数を設定
push: mockPush,
}),
}));
describe('Bar', () => {
it('history.push のテスト', () => {
renderWithRouter(<Bar />, {
route: '/foo/bar',
});
// ここらへんに同期的に副作用を起こす処理を書く
// 以降、副作用で history.push('/foo/baz') されたとする
// 呼ばれたことの確認
expect(mockPush).toBeCalled();
// 引数の確認
expect(mockPush.mock.calls[0][0]).toBe('/foo/baz');
});
});