Next.js + サーバーサイドTypeScript + 関数フレーバーでクリーンなアプリを作ったので実装意図とか書く Advent Calendar 2022
の19日目。株式会社mofmofに生息しているshwldです。
前日はChakraUIとReact Hook FormとZodとについて書きました
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というフィールドが生えており、そいつの配下に扱えるオブジェクトがまとまっています。
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
こうしておくと、viewerをモックするだけで大部分のコンポーネントをレンダリングできます。
モックデータを注入するプロバイダ↓
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
それを使ったコンポーネントテスト↓
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で自動生成できます。
viewerのスキーマが変われば自動でモックデータも更新され、サーバーサイドの変更の影響があるかどうかをsnapshotテストで確認できます。
Storybook
Storybookにテストできる機能があり、そっちに寄せることを検討したのですが、
Storybookのメンテがけっこう大変なので、フロントを組織的に開発するかどうかで必要度は変わって来ると思っていて、今回はStorybookごと見送りました。
テストファイルの自動生成
テストファイル自体は自動で生成しています。
PLOPを使って自動生成しています(PLOPについては以前記事を書いたのでどうぞ)
yarn g:component
などで、コンポーネントを生成できるようにしているのですが、その際にsnapshotテストを実行できる状態のテストファイルを生成できるようにしています。
PLOPは本当に便利なので、どんどん使っていきたい。
次回予告
明日はNode.jsでMailgunを使ってメールを送信するについて書きます。