ts-auto-mockでinterfaceからテスト用データのモック生成を楽にする
typescript react redux nextで作成してる際に、下記のようなコンポーネントのテストを行いたい事がありました。
このコンポーネントのテストをするには、actions/login/routerをそれぞれ正しく型を満たした、propsを用意する必要がありますが、今回テストを死体箇所としては、ログイン状態とリダイレクト関連の箇所だけでactionsを用意する必要はないし、ほかもいくつか必須のプロパティはあるのですが、今回のテストには不要だったため、モック用のデータ生成をinterfaceをもとに最低限の実装で済ませたいと考えていました。
import * as React from 'React'
import { NextRouter } from 'next/router';
interface Props {
actions: LoginActionsInterface
login: LoginStateInterface
router: NextRouter
}
const Login: React.FC<Props> = props => {
const { login, router } = props;
if (login.isLogin) {
const { next } = router.query;
const path = next ? decodeURI(next as string) : '/';
Router.push(path);
return null
}
return <div />
}
ts-auto-mockを使って、使わないデータはinterfaceから自動生成する
そのためのライブラリを探したところ、以下の2つのライブラリがを使用して実装することができました。
1つ目がts-auto-mockで、2つ目のものは、ts-auto-mockをjestで使いやすいように拡張したものです。
jestで使用するには、この2つをインストールすることでかんたんにテスト用のモックデータを生成できるようになります。全体的に設定変更する必要があるのが、少し面倒ではありますが、jest用のreadmeの通りに設定・インストールを行うことで、モック生成可能になります。
https://github.com/Typescript-TDD/ts-auto-mock
https://github.com/Typescript-TDD/jest-ts-auto-mock
こちらが、テストコードですが、Router.pushを持っくし、必要最低限のpropsのデータをモックすることで、テストを実行することができるようになりました。
import * as React from 'react';
import { render, cleanup } from '@testing-library/react';
import { createMock } from 'ts-auto-mock';
import Router, { NextRouter } from 'next/router';
import { LoginActionsInterface, LoginStateInterface } from '@/modules/loginModules';
import Login from './Login';
const base = {
actions: createMock<LoginActionsInterface>(),
login: createMock<LoginStateInterface>({
isLogin: true,
}),
router: createMock<NextRouter>(),
};
jest.mock('next/router');
describe('Login', () => {
beforeEach(() => {
Router.push = jest.fn()
});
afterEach(() => {
cleanup();
});
it('ログイン状態になっていたらトップにリダイレクトする', () => {
const props = { ...base };
render(
<Login {...(props)} />
);
expect(Router.push).toHaveBeenCalledTimes(1);
expect(Router.push).toHaveBeenCalledWith('/');
});
it('nextパラメーターを持ってたらnextをもとに次のページにリダイレクトする', () => {
const next = '/test';
const props = {
...base,
...{
router: createMock<NextRouter>({
query: {
next: encodeURI(next),
},
}),
},
};
render(<Login {...(props as any)} />);
expect(Router.push).toHaveBeenCalledTimes(1);
expect(Router.push).toHaveBeenCalledWith(next);
});
});
ここまでに、上げた実装とは内容が大きく違いますが、ts-auto-mockを使用したサンプル用のリポジトリも作成してます。
下記のように、interface/type/classでは動作しています。
https://github.com/YasushiKobayashi/samples/blob/master/src/jest-ts-auto-mock-sample/src/mock.ts
https://github.com/YasushiKobayashi/samples/blob/master/src/jest-ts-auto-mock-sample/src/mock.spec.ts
サンプル用のリポジトリ
templatesなど上位層のテストがしやすくなる
react/redux/typescriptの構成でatomic designなどで、コンポーネント設計をしているとpages/templatesはバケツリレーの役割が大きくなるため、受け取るpropsのデータ量が大きくなりやすいです。
propsのモック生成が面倒なため、テストを書くのが下位層のコンポーネントよりも後回しになることが多かったのですが、これで上位層のテストも書きやすくなりました。
immutable.jsで直接は使えないけど、anyを使えば使える(2.3.4では、可能になってます。)
また、ts-auto-mockですが、試してみたところ、immutable.jsと併用して使用することはできないようです。immutable.jsは柔軟な型付けができるようにrecord関数などで様々な処理をしているため、モック生成がしにくくなっているのかと思います。
anyを使うことで少し無理矢理ではありますが、以下のような書き方もできるため、immutable.jsと合わせて使うことも可能です。
import { createMock } from 'ts-auto-mock';
import User from '@/models/User'
const base = {
actions: createMock<any>({}),
login: createMock<any>({
user: new User(),
}),
router: createMock<NextRouter>(),
};
it('login page test', () => {
render(<Login {...props} />);
});
ただし、ここまでanyを使っているのであれば、下記のようにも書くことができるので、無理にts-auto-mockを使う必要はあまりないです。
import { createMock } from 'ts-auto-mock';
import User from '@/models/User'
const base = {
actions: {} as any,
login: {
user: new User(),
} as any,
router: createMock<NextRouter>(),
};
it('login page test', () => {
render(<Login {...props} />);
});
typescript3.7以降でバグがあるかも(こちら、ライブラリーの作者からコメントがありましたが、バージョンアップすることで解決されました)
typescript3.7では、target: ESNextを指定しなければ、初期値用のオブジェクトを引数に入れた際に、エラーが発生しビルドすることができません。
interface MockInterface {
a: string
}
// 動かない
const mock = createMock<MockInterface>({a: 'a'})
// 動く
const mock = createMock<MockInterface>()
このように バグもありますが、テストのデータ生成が非常に楽になるので、便利です。
この記事が気に入ったらサポートをしてみませんか?