Fusic Tech Blog

Fusicエンジニアによる技術ブログ

Apollo Serverを使ったGraphQLクエリのテスト
2024/03/29

Apollo Serverを使ったGraphQLクエリのテスト

GraphQLは単一リクエストで複数リソースを取得できたり、APIに型付けされ、スキーマベースでサーバーとクライアント間の疎通がしやすくなるなどとても強力な技術です。

しかし開発時の課題の一つとして出てくるのが、「クライアント側から投げているクエリはサーバーで定義されているスキーマに対して正しいのか?」ということです。

クエリ作成時には正しかったものでも、開発が進みスキーマが変更されていくとあるフィールドが使われなくなり、 @deprecated になり、消えてしまったとします。 そして、その消えてしまったフィールドを利用しているクエリは実行時にはエラーになっていしまいます。

こういったクエリでのエラーを防ぐためにはクライアント側で利用されているクエリが現在のGraphQLスキーマに対して正しいのかチェックする必要があります。

この記事ではフロントエンドではApollo Client、バックエンドではApollo Serverまたは別言語で作られたGraphQLサーバーアプリケーション(PHP, Rubyなど) 構成の場合を想定した。GraphQLクエリをチェックするためのテスト手法を書きたいと思います。

Apollo Client/Apollo Serverの構成だった場合はどちらもNode.js環境でテストを動かせるので、クライアントから投げるクエリとサーバーからくるレスポンスの両方をチェックできる統合テストが書けます。

バックエンドが別言語だった場合は、 Introspection というGraphQLの機能を利用してApollo Serverにスキーマ情報を取り込みモックサーバーとして動かすことで、Node.js環境で完結したクエリチェックテストを書くことができます。

まずはApollo Serverでモックサーバーを用意してテストを書いてみましょう。

Apollo Serverでモックを作る

まず、apollo-serverをインストールします。

$ npm install apollo-server

次に簡単なスキーマを書いてモックサーバーを用意します。

// index.js
const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
  type Query {
    todos: [Todo]
  }

  type Todo {
    id: ID!
    title: String
    body: String
    done: Boolean
  }
`;

const server = new ApolloServer({
  typeDefs,
  mocks: true
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

localhost:4000 にアクセスするとPlaygroundが立ち上がるので試しにクエリを実行してみます。

{
  todos {
    id
    title
    body
    done
  }
}

すると以下のような結果が返ってきます。

{
  "data": {
    "todos": [
      {
        "id": "10bade2d-ffc2-43ce-9bb4-597a475b0fd8",
        "title": "Hello World",
        "body": "Hello World",
        "done": true
      },
      {
        "id": "1e06c063-1944-4493-bb96-abb32e216cb2",
        "title": "Hello World",
        "body": "Hello World",
        "done": true
      }
    ]
  }
}

モックをカスタマイズする

今回はテストで期待する値が返ってきて欲しいので以下のようにカスタマイズします。

const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
  type Query {
    todos: [Todo]
  }

  type Todo {
    id: ID!
    title: String
    body: String
    done: Boolean
  }
`;

const resolvers = {
  Query: {
    todos: () => {
      return [
        {
          id: 1,
          title: "My Todo 1",
          body: "読書をする",
          done: false
        },
        {
          id: 2,
          title: "My Todo 2",
          body: "ジョギングをする",
          done: false
        }
      ];
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

クエリを用意する

$ npm install graphql-tag
// graphql/todo.js

const gql = require("graphql-tag");

module.exports.GET_TODOS = gql`
  query GetTodos {
    todos {
      id
      title
      body
      done
    }
  }
`;

クライアント側で利用するクエリを用意しました。

これらを apollo-client やフレームワークを使うなら react-apollovue-apollo などと一緒に使っていことになるはずです。

例えば、 vue-apollo を使った場合だと

// TodoList.vue

<template>
  <div>
    <ul>
      <li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
    </ul>
  </div>
</template>

<script>
import { GET_TODOS } from './graphql/todo'

export default {
  name: "TodoList",
  apollo: {
    todos: {
      query: GET_TODOS,
    }
  },
  data () {
    return {
      todos: []
    }
  }
};
</script>

<style scoped>
</style>

このように読み込んでデータ取得表示できます。

テストを書いてみる

$ npm install jest apollo-server-testing

テストでApolloServerインスタンスが必要になるため、別ファイルに切り離しておきます。

// apollo-server.js

const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
  type Query {
    todos: [Todo]
  }

  type Todo {
    id: ID!
    title: String
    body: String
    done: Boolean
  }
`;

const resolvers = {
  Query: {
    todos: () => {
      return [
        {
          id: 1,
          title: "My Todo 1",
          body: "読書をする",
          done: false
        },
        {
          id: 2,
          title: "My Todo 2",
          body: "ジョギングをする",
          done: false
        }
      ];
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

module.exports.server = server;

// index.js

const { server } = require("./apollo-server");

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

テストの準備が整ったので、Jestを使って試しにスナップショットテストを書いてみます。

// tests/todo.spec.js

const { createTestClient } = require("apollo-server-testing");
const { server } = require("../apollo-server");
const { GET_TODOS } = require("../graphql/todo");

describe("My Todo Test", () => {
  it("gets todos", async () => {
    const { query } = createTestClient(server);
    const res = await query({ query: GET_TODOS });
    expect(res).toMatchSnapshot();
  });
});

以下のような結果になりました。

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`My Todo Test gets todos 1`] = `
Object {
  "data": Object {
    "todos": Array [
      Object {
        "body": "読書をする",
        "done": false,
        "id": "1",
        "title": "My Todo 1",
      },
      Object {
        "body": "ジョギングをする",
        "done": false,
        "id": "2",
        "title": "My Todo 2",
      },
    ],
  },
  "errors": undefined,
  "extensions": undefined,
  "http": Object {
    "headers": Headers {
      Symbol(map): Object {},
    },
  },
}
`;

Introspectionを使ったモック

一通りApollo Serverとクエリの統合テストを行ってみました。

しかし、GraphQLサーバーアプリケーションがNode.jsで作られたものではない場合(例えばgraphql-rubyやgraphql-phpを使ったものなど)、クライアント側のクエリがバックエンドで定義されているスキーマと合っているかテストするのは難しいです。

そこで、Apollo Serverには Introspection を使ってモックサーバーを立てることが可能です。

Introspectionクエリはバックエンド側で定義されたGraphQLのスキーマ情報を取得できるクエリで、その結果をApollo Serverに読み込ませ、モックサーバーとして動かすことでクライアント側クエリのテストを実現できます。

Apollo CLIを使ってIntrospectionクエリ結果を出力

Apollo CLIを使うことでGraphQLサーバーのエンドポイントからIntrospectionクエリの結果をjsonファイルとして出力します。

まず、Apollo CLIをグローバルにインストールします。

$ npm install -g apollo

今回はNode.jsアプリケーションですが、別言語でのアプリケーションが localhost:4000 のエンドポイントにサーバーが動いていると仮定して以下コマンドで出力できます。

$ apollo client:download-schema --endpoint=http://localhost:4000 schema.json

スキーマ情報が書かれているschema.jsonが生成されます。

スキーマを基にモックサーバーを用意する

スキーマ情報をNode.jsでも扱えるjsonファイルにすることができました。

これを graphql パッケージの buildClientSchema() を使ってGraphQLSchemaオブジェクト生成、それをApollo Serverオプションとして読み込ませることで、Node.jsで完結したGraphQLサーバーのモックが完成します。

それではapllo-server.jsをschema.jsonを読み込む形に編集してみます。

const { ApolloServer } = require("apollo-server");
const { buildClientSchema } = require("graphql");
const introspectionResult = require("./schema.json");

const schema = buildClientSchema(introspectionResult);

const server = new ApolloServer({
  schema,
  mocks: true
});

module.exports.server = server;

この状態でサーバーを起動すると、最初に動かしたような"Hello World" の文字列などが返ってくるレスポンスになっていると思います。

今回はクライアント側のクエリがスキーマに対して正しいかどうかチェックできればいいので、mocksオプションから型ごとに固定の値を返すことができます。

const mocks = {
  ID: () => 12,
  Int: () => 123,
  Float: () => 123.456,
  String: () => "テスト"
};

const server = new ApolloServer({
  schema,
  mocks
});

クエリの実行結果は以下のようになります。

{
  "data": {
    "todos": [
      {
        "id": "12",
        "title": "テスト",
        "body": "テスト",
        "done": false
      },
      {
        "id": "12",
        "title": "テスト",
        "body": "テスト",
        "done": false
      }
    ]
  }
}

これで apollo-server-testingJest でテストを書くと、何かしらバックエンド側でスキーマが変更あったなどして、クライアント側で使っているクエリとで齟齬があった場合に気づくことができます。

まとめ

まず最初に、フロントエンドはApollo Clientで、バックエンドがNode.js(Apollo Server)の構成での apollo-server-testingJest を使った統合テストを行ってみました。

次に、フロントエンドとしてApollo Client、バックエンドでRubyやPHPなどの別言語を想定した場合のクエリテストを行いました。

こういったテストを行うことでクライアントアプリで投げているGraphQLクエリが(少なくともAPIレベルでは)サーバー側とコミュニケーションが正確にとれていること担保できます。

おまけ: ESLintでのクエリチェック

実はすでに eslint-plugin-graphql というものがあり、ESLintでスキーマに対しての.gqlや.jsファイルで書かれたクエリのチェックは可能です。

こちらの方がエディタのESLint拡張と組み合わせることで編集時にエラーが出たり、補完が効いたりと便利です。

しかし、これはあくまでLinterなので書かれているクエリ情報までしかチェックできず、MutationでのInputタイプの中まではチェックされません。

例えば、

input TodoInput {
  title: String
  body: String
}

type Todo {
  id: ID!
  title: String
  body: String
}

type Mutation {
	createTodo(input: TodoInput): Todo
}

とバックエンド側でスキーマが定義されていた場合、

クライアント側では、

mutation CreateTodo($input: TodoInput) {
	createTodo(input: $input) {
		id
		title
		body
	}
}

といったクエリが投げられたとします。

この時に渡ってくる variables が以下だったとします。

{
	"title": "読書",
	"content": "一時間読書をする"
}

この場合 titlebody を渡さないといけないですが、content という間違った値が渡された場合はGraphQLエラーになります。

これはESLintではチェックできません。

そこで今回行ったようにモックのApollo Serverを用意し、そこに対してクエリを投げてやることで variables までを考慮されるクエリがパースされた後のものでチェックが入ります。

そのため、シンプルなQuery/MutationはESLintでチェックし、複雑なInputタイプを持つMutationはしっかりテストを書いておくのがいいかもしれません。

Daiki Urata

Daiki Urata

フロントエンド/モバイルアプリなどを主に開発しています。