🏭

Apollo Server & Firestoreで Integration Test する

2021/04/01に公開

はじめに

Firestore をデータベースとして Apollo Server を利用している場合、Integration Test としては、Firestore エミュレータを利用したテストが、実際の Firestore からのデータ取得を想定して扱えるためにとても便利です。

今回は、 Apollo Server への Firestore 導入と、Firestore エミュレータを導入した Integration Test の実行方法を紹介します。

ターゲットとなる方

  • Firestore を使っていて、これから BFF として GraphQL 採用を考えている方
  • Apollo Server で Firestore をデータベースとして利用していて、テスト導入を検討している方

説明しないこと

  • Firestore エミュレーターの構築と実行手順
    • こちらは記事の中で、参考記事を紹介させていただきます
  • Apollo Server の構築
    こちらは割愛させていただきますが、もしこれから構築という方は、僕が個人で開発しているテンプレートなどをご利用いただくと便利かもしれません 🙇‍♂️

https://github.com/shoNagai/apollo-server-micro-boilerplate

Apollo Server に Firestore への接続を追加する

Apollo Server で Firestore に接続するために、firebase-admin をインストールします。

$ yarn add firebase-admin

Apollo Server から Firestore を参照させる方法は、firebase-admin を導入したら、利用したいファイルで、import するだけでもそのままお使いいただけます。
firebase-admin の初期化処理は必要になります)

import * as admin from "firebase-admin";

const db = admin.firestore();

const books = await db.collection(`books`).get();

上記みたいな形ですぐに書き始めることができるのですが、こちらはあまりおすすめしません。

Apollo Server の datasources として利用することで、モック化や今回紹介する Firestore エミューレータの利用など差し替えが便利になります。

まずは、Firestore 接続用の datasource を作成します。

firestoreDatasource.ts
import { DataSource } from 'apollo-datasource';
import { firestore } from 'firebase-admin';

export default class FirestoreDatasource extends DataSource {
  public db: firestore.Firestore;

  constructor(firestore: firestore.Firestore) {
    super();
    this.db = firestore;
  }

  // とりあえず特定のCOLLECTIONをgetするだけの関数
  public getAll = async <T>(collectionPath: string) => {
    const snaps = await this.db.collection(collectionPath).get();
    if (snaps.empty) return [];
    return snaps.docs.map((doc) => { ...(doc.data() as T), id: doc.id });
  };
}

作成した datasource を、Apollo Server 初期化時に、dataSources として設定します。
(この時、firebase-admin の初期化処理を忘れずに追加します)

import * as admin from "firebase-admin";
import FirestoreDatasource from "./firestoreDatasource";
import { ApolloServer } from "apollo-server-micro";

admin.initializeApp({
  databaseURL: `https://${process.env.PROJECT_ID}.firebaseio.com`,
});

const dataSources = () => ({
  firestore: new FirestoreDatasource(admin.firestore()),
});

const apolloServer = new ApolloServer({ schema, dataSources });

これで Apollo Server 起動時に、各 resolvers から Contextの引数として Firestore へのアクセスを dataSources 経由で行えるようになります。

さっそく、Firestore から books のコレクションを返す Query を定義してみます。

import { ApolloError } from "apollo-server-micro";
import { Book } from "../repositories/book";
import { Resolvers } from "../types/graphql";

export const resolvers: Resolvers = {
  Query: {
    async books(_, _args, { dataSources: { firestore } }) {
      try {
        return firestore.getAll<Book>(`books`);
      } catch (error) {
        console.error(error);
        throw new ApolloError(error);
      }
    },
  },
};

Apollo Server にテスト環境の設定する

Apollo Server のテストとしては、apollo-server-testing を利用していきます。
また、Firestore のテストライブラリである @firebase/testing もあわせてインストールします。

$ yarn add -D apollo-server-testing @firebase/testing

次に、Firestore エミュレータをローカル環境に構築して、準備完了となります。
Firestore エミュレータの環境構築は、下記の記事がわかりやすくまとまっておりました!
https://zenn.dev/ginpei/articles/firebase-firestore-emulator

テストを書いていく

テストファイルに、firebaseTesting と Apollo Server の初期化を記述します。
ここで、datasources として Firestore を設定することで、初期化処理に、firebaseTesting から取得した Firestore エミュレータ を指定することができます。

book.ts
import * as firebaseTesting from "@firebase/testing";
import { ApolloServer } from "apollo-server-micro";
import { importSchema } from "graphql-import";
import FirestoreDatasource from "../../../datasources/firestoreDatasource";
import { resolvers } from "../../../resolvers";

const adminApp = firebaseTesting.initializeAdminApp({
  projectId: `apollo-server-with-firestore`,
});

const firestore = adminApp.firestore();

const server = new ApolloServer({
  typeDefs: importSchema("src/schemas/schema.graphql"),
  resolvers: resolvers,
  dataSources: () => ({
    firestore: new FirestoreDatasource(firestore as any),
  }),
  introspection: true,
});

次に、 apollo-server-testing の createTestClient を使って、Apollo Server からテスト用の Apollo Client を生成します。
あとは、テストデータを事前に Firestore に挿入、Apollo Client を使って Query を実行し、取得データの確認といったテストを記述していきます。

book.ts
import { gql } from "apollo-server-micro";
import { createTestClient } from "apollo-server-testing";

const BOOKS = gql`
  query books {
    books {
      id
      title
      author
    }
  }
`;

const NEW_BOOK: Book = {
  id: `I37BLody5Vj8Yux8vNg9`,
  title: `ブルーピリオド`,
  author: `山口つばさ`,
};

describe("book resolver test", () => {
  describe("正常系", () => {
    beforeEach(async () => {
      // テストデータの挿入
      const store = new FirestoreDatasource(firestore as any);
      await store.set(bookPath(), NEW_BOOK.id, NEW_BOOK);
    });

    it("書籍一覧のの取得", async () => {
      const { query } = createTestClient(server);
      const res = await query<{ books: Book[] }>({ query: BOOKS });

      // クエリ結果の確認
      expect(res.data?.books[0].id).toBe(NEW_BOOK.id);
      expect(res.data?.books[0].title).toBe(NEW_BOOK.title);
      expect(res.data?.books[0].author).toBe(NEW_BOOK.author);
    });
  });
});

テストを実行する

コンソールを2つ立ち上げます。

まず片方では、エミュレーターを起動します。

$ yarn emulators:start

もう片方のコンソールでは、テストを実行します。

$ yarn test:local

まとめ

Apollo Server で Firestore を利用する場合、datasources として利用することで、テスト利用やエミュレーター利用など、開発が便利になります。

また、Integration Test だけでなく、エミュレータを利用することで、動作を確認しながら開発していく TDD での開発も行っていけるかなと思います。

今回、jest 設定など一部設定は省略させて頂きましたので、フルでご確認したい場合は、下記リポジトリにてご確認ください。

https://github.com/shoNagai/apollo-server-with-firestore

参考

Gaudiy Engineers' Blog

Discussion