🔎

GraphQLのSchemaを元にサクッとモックサーバーを作る

2023/07/29に公開

概要

GraphQLでクライアントの開発をしており下記のような理由からモックサーバーが欲しくなりました。

  • バックエンドの開発を待たずにGraphQLのSchemaベースで非同期でフロントエンドの開発を行いたい
  • テスト用にAPIのモック

サクッと良い感じにモックサーバー建てられないかな〜と思って調べてみました。

作りたいもの

GraphQLのSchemaを正にして、可能な限り自動で型セーフなMSWのハンドラ+データ作成関数を作りたい。
これによってShemaの変更に対してモックサーバもすぐに追随できる+モックサーバのメンテナンスもしやすいのかなと思いました。

ざっくりと導入手順

下記にやったことをざっくりと書いていきます。

まず、下記のようなGraphQL SchemaとQueryがあると仮定します。

Schema

type Query {
  getUser(id: ID!): User
}

type User {
  id: ID!
  name: String!
  email: String!
}

Query

query GetUser($id: String!) {
  getUser(id: $id) {
    id
    name
    email
  }
}

1. 必要なパッケージをインストール

まず初めに、必要なパッケージをインストールします。

npm install @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-msw graphql-codegen-typescript-mock-data msw

それぞれの用途は下記です。

2. GraphQL Code Generatorの設定

次にcodegen.ymlファイルを以下のように記述します。

const config: CodegenConfig = {
  overwrite: true
  schema: "path/to/your/schema.graphql" // 適当な値を入れる
  documents: '**/*.graphql', // 適当な値を入れる
  generates:
   /**
   * その他のgeneratorsは省略
   */
   '__generated__/mockHandlers.ts': {
     plugins: [
       'typescript',
       'typescript-operations',
       '@graphql-codegen/typescript-msw',
        {
         'graphql-codegen-typescript-mock-data': {
           prefix: 'mock',
        },
      },
    ],
  },
}

この設定により、コード生成コマンドを実行するとTSの型MSWのハンドラ、そしてモックデータ生成関数がそれぞれ記載された下記のようなファイルが生成されます。

自動生成されたファイルの内容
import { graphql, ResponseResolver, GraphQLRequest, GraphQLContext } from 'msw'
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: { input: string; output: string; }
  String: { input: string; output: string; }
  Boolean: { input: boolean; output: boolean; }
  Int: { input: number; output: number; }
  Float: { input: number; output: number; }
};

export type Query = {
  __typename?: 'Query';
  getUser?: Maybe<User>;
};


export type QueryGetUserArgs = {
  id: Scalars['ID']['input'];
};

export type User = {
  __typename?: 'User';
  email: Scalars['String']['output'];
  id: Scalars['ID']['output'];
  name: Scalars['String']['output'];
};

export type GetUserQueryVariables = Exact<{
  id: Scalars['ID']['input'];
}>;


export type GetUserQuery = { __typename?: 'Query', getUser?: { __typename?: 'User', id: string, name: string, email: string } | null };


/**
 * @param resolver a function that accepts a captured request and may return a mocked response.
 * @see https://mswjs.io/docs/basics/response-resolver
 * @example
 * mockGetUserQuery((req, res, ctx) => {
 *   const { id } = req.variables;
 *   return res(
 *     ctx.data({ getUser })
 *   )
 * })
 */
export const mockGetUserQuery = (resolver: ResponseResolver<GraphQLRequest<GetUserQueryVariables>, GraphQLContext<GetUserQuery>, any>) =>
  graphql.query<GetUserQuery, GetUserQueryVariables>(
    'GetUser',
    resolver
  )


export const mockQuery = (overrides?: Partial<Query>): Query => {
    return {
        getUser: overrides && overrides.hasOwnProperty('getUser') ? overrides.getUser! : mockUser(),
    };
};

export const mockUser = (overrides?: Partial<User>): User => {
    return {
        email: overrides && overrides.hasOwnProperty('email') ? overrides.email! : 'sunt',
        id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : 'a5756f00-41a6-422a-8a7d-d13ee6a63750',
        name: overrides && overrides.hasOwnProperty('name') ? overrides.name! : 'porro',
    };
};

3. 自動生成生成されたコードを使用する

後は下記のように作成されたコードを組み合わせるだけとなります。

  • mockGetUserQuery: 自動生成されてたmswのハンドラー
  • mockUser: 自動生成されたダミーデータ作成関数

※ ダミーデータ作成関数は当然オーバーライド可能で、より現実的な値を設定することもできます

import { mockGetUserQuery, mockUser } from '__generated__/mockHandlers'

const handlers = [
  mockGetUserQuery((req, res, ctx) => {
    const { id } = req.variables;
    return res(
      // Schemaに応じて適当なデータが作成される
      ctx.data({ mockUser() })
    )
  })
]

const worker = setupWorker(...handlers)
tacomsテックブログ

Discussion