VitestでGraphQLとReactをテストするための環境づくり

[Next.js + サーバーサイドTypeScript + 関数フレーバーでクリーンなアプリを作ったので実装意図とか書く Advent Calendar 2022](https://qiita.com/advent-calendar/2022/shwld)
の**19日目**。[株式会社mofmof](https://www.mof-mof.co.jp/)に生息している[shwld](https://twitter.com/shwld)です。

前日は[ChakraUIとReact Hook FormとZodと](https://qiita.com/shwld/items/d5b7f8e64ea7cf4871c8)について書きました
# Vitestでテストする

モノレポの中で、複数パッケージを扱っており、パッケージによってテストツールも多少の違いがあります。

| パッケージ | 利用しているテストツール |
|----|----|
| apps/web (Next.js) | Jest (Next.jsが最初からセットアップ済の `next/jest` |
| apps/worker (バックグラウンドワーカー)) | なし |
| domain/core (ドメインモデル) | Vitest |
| infrastructures/* (インフラ系) | なし |
| use-cases/graphql-resolvers (GraphQLサーバーの実装) | Vitest |

基本的にはVitestを使っており、Next.jsだけデフォルトでテストの設定があるのでそれを使っています。

## テストファイルの場所
テスト用のディレクトリを切ってまとめるか、コンポーネントや関数ファイルの隣に置くかがあると思いますが、後者を選択しています。

例
```
- src/models/account/mutations/build-account.ts
- src/models/account/mutations/build-account.test.ts
```

こちらのメリットは単純で、処理とテストをセットで直しやすいというところですが、クリーンなアーキテクチャを目指す上で、テストの依存が同じパッケージに入り込むというデメリットがあります。
まあでもそれよりも直しやすいほうが大事かなと言う判断。

## インフラのテストがない
これは優先度が下がっていて実装してないです。
例えばRepositoryの実装だけをテストするのは必要だと思いますが、use-caseなどと結合した際のテストのほうが重要だと思っています。
use-caseのテストでDBをモックするか実DBを使うかみたいな話。
モックはせず実DBを使ってテストをする方針としており、Repository自体のテストは端折っています。
mailerなどはモックしてテストしているのでやった方がいいが、コード自体にに複雑さはないので、まだテストはないです。

## コンポーネントのテスト
コンポーネントは[Next.jsのコンポーネント構造]()で書きましたが、コンテナーとプレゼンテーションをわけていないので、基本的にGraphQL APIへの依存がコンポーネントに入り込んでいることが多いです。
なので、モックしてテストしているのですが、ここはGraphQLサーバー側の設計で、Queryのルートにviewerというフィールドが生えており、そいつの配下に扱えるオブジェクトがまとまっています。

```ts:/use-cases/graphql-resolvers/src/modules/viewer/object-resolvers/viewer/viewer.sdl.graphql
type Viewer {
  id: ID!
  email: String!
  profile: UserProfile!
  createdAt: DateTime!
  updatedAt: DateTime!
  accounts(first: Int, after: String, page: Int): AccountConnection!
  project(id: ID!): Project
  invitationToken(confirmationToken: String!): ProjectMemberInvitationToken
}
```
ソースコード: [/use-cases/graphql-resolvers/src/modules/viewer/object-resolvers/viewer/viewer.sdl.graphql](https://github.com/shwld/uzumaki/blob/advent-calendar/use-cases/graphql-resolvers/src/modules/viewer/object-resolvers/viewer/viewer.sdl.graphql)

こうしておくと、viewerをモックするだけで大部分のコンポーネントをレンダリングできます。

モックデータを注入するプロバイダ↓

```ts:/apps/web/src/test/MockedUrqlProvider.tsx
import { ReactNode, FC } from 'react';
import { Provider } from 'urql';
import { fromValue, never } from 'wonka';
import { aViewer } from '~/graphql/generated/mockData';

export const MockedUrqlProvider: FC<{
  children?: ReactNode;
  executeQuery?(): any;
  executeMutation?(): any;
  executeSubscription?(): any;
}> = ({ children, executeQuery, executeMutation, executeSubscription }) => (
  <Provider
    value={
      {
        executeQuery:
          executeQuery ??
          (() =>
            fromValue({
              data: {
                viewer: aViewer(),
              },
            })),
        executeMutation: executeMutation ?? jest.fn(() => never),
        executeSubscription: executeSubscription ?? jest.fn(() => never),
      } as any
    }
  >
    {children}
  </Provider>
);
```
ソースコード: [/apps/web/src/test/MockedUrqlProvider.tsx](https://github.com/shwld/uzumaki/blob/advent-calendar/apps/web/src/test/MockedUrqlProvider.tsx)

それを使ったコンポーネントテスト↓

```ts:
import { ProjectBoard } from './ProjectBoard';
import { render } from '@testing-library/react';
import { MockedUrqlProvider } from '~/test/MockedUrqlProvider';

describe('ProjectBoard', () => {
  const renderComponent = () => {
    const renderResult = render(
      <MockedUrqlProvider>
        <ProjectBoard projectId="test" />
      </MockedUrqlProvider>
    );
    return renderResult;
  };
  test('Snapshot', () => {
    expect(renderComponent().asFragment()).toMatchSnapshot();
  });
  test('success', () => {
    const { getByText } = renderComponent();
    expect(getByText('Current')).toBeTruthy();
  });
});
```

### モックデータの自動生成
モックデータ自体も [GraphQL Code Generator](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-mock-data)で自動生成できます。
viewerのスキーマが変われば自動でモックデータも更新され、サーバーサイドの変更の影響があるかどうかをsnapshotテストで確認できます。

## Storybook
Storybookにテストできる機能があり、そっちに寄せることを検討したのですが、
Storybookのメンテがけっこう大変なので、フロントを組織的に開発するかどうかで必要度は変わって来ると思っていて、今回はStorybookごと見送りました。

## テストファイルの自動生成
テストファイル自体は自動で生成しています。
[PLOP](https://plopjs.com/)を使って自動生成しています（PLOPについては以前記事を書いたのでどうぞ）

https://zenn.dev/shwld/articles/c26345ed8e781b


```bash
yarn g:component
```

などで、コンポーネントを生成できるようにしているのですが、その際にsnapshotテストを実行できる状態のテストファイルを生成できるようにしています。

PLOPは本当に便利なので、どんどん使っていきたい。
# 次回予告
明日は[Node.jsでMailgunを使ってメールを送信する](https://qiita.com/shwld/items/054e214dd7800ac1b1e3)について書きます。