📕

Apollo ClientのモックとE2Eテスト(w/ Storybook)

2022/08/16に公開

概要

  • Storybook駆動での開発・テストは便利!
  • StorybookでのApollo Clientのモックは、SchemaLinkを使った方法が個人的におすすめ
  • モノレポ構成で開発をしている場合は、バックエンドのモジュールをモックに使うと、StorybookでE2Eテストっぽいこともできる
    • ※この部分はかなり実験的な内容になります。また、厳密なE2Eテストではありません

Storybookによる開発とテスト

Storybook、便利ですね。特に最近利用可能になったPlay Functionが素晴らしいなと感じています。
Play Functionを使うと、以下のようなインタラクションを表示・テストすることが可能になります。(動画は公式より)

この記事の本旨ではないので詳しい説明は省きますが、興味がある方はぜひ以下を読んでみてください。

https://zenn.dev/takepepe/articles/storybook-driven-development

https://zenn.dev/azukiazusa/articles/df307292037265

Apollo Clientのモック

HTTPリクエストを行うコンポーネントをStorybookで利用する際は、開発・テスト中にHTTPリクエストが飛ばないよう、通信をモックする必要があります。
GraphQLクライアントでApollo Clientを使っている場合に、どんなモックの方法があるか紹介します。

MockedProviderによるモック

MockedProviderでモックするのが公式の推奨のようです。

https://www.apollographql.com/docs/react/development-testing/testing/

StorybookのAddonもあります。

https://storybook.js.org/addons/storybook-addon-apollo-client

この方法は悪くないのですが、個人的にはやや使いづらく感じました。

  • モックの判定が「完全一致」しかなく、「部分一致」がない。
    • これにより、「このクエリはvariablesは無視して一定の値を返す」のような書き方ができない
  • モックしたいリクエストを「全て」書く必要がある。「よしなに」モックはしてくれない(またその方法がない)。
  • モックを書くときに型情報がつかえない(頑張ればできるかも)

次節のSchemaLinkを使うと、このデメリットが克服できます。

SchemaLinkによるモック

Apollo Clientの魅力の1つは、Apollo Linkにより柔軟かつシンプルな拡張性が担保されていることでしょう。
HTTP通信を行うレイヤもLinkにより抽象化されており、通常だと HttpLink を使うことになりますが、なんとここをSchemaLinkに置き換えるだけで、通信をモックすることができます。

SchemaLink には GraphQLSchema 型の引数を与える必要がありますが、これをgraphql-toolsmakeExecutableSchemaaddMocksToSchemaにより生成することで、柔軟なモック生成が可能になります。

import { makeExecutableSchema } from '@graphql-tools/schema'
import { addMocksToSchema } from '@graphql-tools/mock'

// GraphQLスキーマの文字列
// (実際は、graphql-codegenを使って生成したスキーマを読み込むのが良い)
const schemaString = /* GraphQL */ `
# ...
`

// スキーマの生成
const schema = makeExecutableSchema({ typeDefs: schemaString })

// スキーマをモック
const schemaWithMocks = addMocksToSchema({
  schema,
  mocks: {
    // モックを定義(定義しなかった型は適当な値が自動生成されるので、必須ではない)
    User: () => ({
      name: 'ユーザー名',
    }),
    // カスタムスカラーはここで定義が必要
    DateTime: () => '2011-01-05T17:08:49.000-0430',
  },
  // mockではなく、リゾルバを定義してより動的にレスポンスを返すことも可能
  resolvers: {
    Query: {
      users: async (ref, variables) => {
        // ...
      }
    }
  }
})

// ApolloClientの作成
const client = new ApolloClient({
  link: new SchemaLink({ schema: schemaWithMocks }),
})

この方法のメリット

  • デフォルトだと addMocksToSchema が「よしなに」モックデータを生成してくれる👏
    • String型ならString型の、Date型ならDateの値を適当に生成して返してくれるので、シンプルな構造のオブジェクト・テストならデータの準備が不要
  • 必要に応じてResolverをテスト側で定義できる
    • これにより、どんな形でのモックも可能。 variablesに応じた値を返したり、一旦無視して雑にレスポンスを返したりなどが自由。
  • graphql-codegenで生成した型を使うことで、型の情報を使いながらリゾルバのモックを書ける
Resolver型の定義方法
import { IMockStore } from '@graphql-tools/mock'
// graphql-codegenが生成した型情報
import { Mutation, Query } from '../../generated'

type QueryResolvers = {
  [QueryName in keyof Query]: (
    ref: unknown, // 親が入ってくる https://www.graphql-tools.com/docs/mocking#mocking-a-pagination
    variables: Record<string, unknown>, // graphql-codegenの出力をいじれば、ここにも型をつけることが可能そう。ここでは省略
  ) => Query[QueryName] | Promise<Query[QueryName]>
}
type MutationResolvers = {
  [MutationName in keyof Mutation]: () => Mutation[MutationName] | Promise<Mutation[MutationName]>
}

type Resolvers = (store: IMockStore) => {
  Query?: Partial<QueryResolvers>
  Mutation?: Partial<MutationResolvers>
}

mswによるモック

「クライアントをモックするなんてけしからん😤 本番に近い形でテストをするために、Webサーバー側をモックすべき!」という方は、mswを使ってモックするのが良さそうです。
例えば、以下の記事のような形でモックができます。

https://zenn.dev/ynakamura/articles/5d92bd34a363c6

この構成も十分に良いと考えてますが、強いて言えば少し冗長な構成になると感じています。
強い必要性がなければよりシンプルな方法であるSchemaLinkによるモックが個人的には好みです。

バックエンドのロジックでモックしてE2Eテスト(モノレポ+DI)

※ここから先はかなり実験的な内容、ないし半分ネタです。

さて、ここまででSchemaLinkによりリゾルバーを自由に書くことが可能になりました。
しかし、複雑な処理をモックするとなると、なかなかモックを書くのが大変です。

そういえばモノレポ構成であれば、バックエンドのコードをモックから呼ぶことも可能そうです。
試してみましょう。

import { addMocksToSchema } from '@graphql-tools/mock'
import { newDb } from "pg-mem"; // PostgreSQLのモック
import { UsersQueryResolver } from "@backend/presentation/web"; // バックエンドで定義したリゾルバー

const db = newDb();

const schemaWithMocks = addMocksToSchema({
  schema,
  resolvers: (_store) => ({
    Query: {
      users: async ({ variables }) => {
        return await UsersQueryResolver.resolve(variables, { db }) // contextにdbを渡してDI
      },
    },
  }),
})

const mockedApolloClient = new ApolloClient({
  link: new SchemaLink({ schema: schemaWithMocks }),
})

ここまでやると、もはや実行されるコードはほぼ実際のコードと変わらなくなってくるため、冒頭のPlay Function+Jestと合わせてE2Eライクなテストが可能になります。👏👏
従来のE2Eテストと比べて、テスト内容を目視で確認することがしやすく、またデータベースが不要であり並列化・高速化もしやすいといったメリットがあり、とても便利です。

また、開発中はバックエンドのコードを編集するとStorybookの画面がリロードされるので、Storybookの画面を見ながらバックエンドのコードを改修するといった開発方法で便利に開発ができます。

実現が難しい点

便利!………と、言いたいところなのですが、上記ほど綺麗な形で書くには、以下の条件が揃う必要があります。

  • バックエンドもTypeScriptであること
  • モノレポであり、バックエンドのリゾルバをインポートできること
  • DBクライアントをDIできること
  • DBクライアントをモックできること
    • 特に、Storybook(=ブラウザ)上で動く形でモックすること
    • 上記ではサンプルとしてpg-memを使っていますが、残念ながらブラウザでは動きません…

ということで、なかなか厳しそうですね。
また、あくまでE2Eテスト「っぽい」ものであり、いくつかの要素がテストできないこと、また特殊な構成であることに注意しましょう。

とはいえ、弊社環境だと一部ロジックについてはこの構成の実現ができており、実際に開発で利用しておりとても便利に感じています。
DBのモックについては、ブラウザで動くPostgreSQLエミュレーターを自作してます。一日で書いたレベルの出来ですが、割と便利です。

興味がある方はぜひチャレンジしてみてください。

Discussion