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>()

このように バグもありますが、テストのデータ生成が非常に楽になるので、便利です。


この記事が気に入ったらサポートをしてみませんか?