Apollo Server & Firestoreで Integration Test する
はじめに
Firestore をデータベースとして Apollo Server を利用している場合、Integration Test としては、Firestore エミュレータを利用したテストが、実際の Firestore からのデータ取得を想定して扱えるためにとても便利です。
今回は、 Apollo Server への Firestore 導入と、Firestore エミュレータを導入した Integration Test の実行方法を紹介します。
ターゲットとなる方
- Firestore を使っていて、これから BFF として GraphQL 採用を考えている方
- Apollo Server で Firestore をデータベースとして利用していて、テスト導入を検討している方
説明しないこと
- Firestore エミュレーターの構築と実行手順
- こちらは記事の中で、参考記事を紹介させていただきます
- Apollo Server の構築
こちらは割愛させていただきますが、もしこれから構築という方は、僕が個人で開発しているテンプレートなどをご利用いただくと便利かもしれません 🙇♂️
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 を作成します。
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 エミュレータの環境構築は、下記の記事がわかりやすくまとまっておりました!
テストを書いていく
テストファイルに、firebaseTesting と Apollo Server の初期化を記述します。
ここで、datasources として Firestore を設定することで、初期化処理に、firebaseTesting から取得した Firestore エミュレータ を指定することができます。
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 を実行し、取得データの確認といったテストを記述していきます。
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 設定など一部設定は省略させて頂きましたので、フルでご確認したい場合は、下記リポジトリにてご確認ください。
参考
Web3スタートアップ「Gaudiy(ガウディ)」所属エンジニアの個人発信をまとめたPublicationです。公式Tech Blog:techblog.gaudiy.com/
Discussion