EY-Office ブログ

Apollo GraphQLサーバーのテストけっこう面倒だった

Apollo GraphQLに入門してみた(おまけ)、リファクタリング・テスティングに書いたApollo GraphQLサーバーの学習をかねたプロトタイプ作成が完了し、それを基に本番サーバーを作りました。

しかし、テスティングの部分でいくつか不足点が見つかりコードを追加したりしました。

testing-the-apollo-graphql-server

テスティング環境

Apollo GraphQLに入門してみた(おまけ)、リファクタリング・テスティングにも書きましたがテスティング環境は、

  • テスティングフレームワークとしてはReactでは標準的なJestを使用
  • サーバーはExpressの上で動いているので、Expressの豊富なライブラリーが利用できます。今回はSuperTestというExpress用のテストライブラリーを使っています
  • SuperTestはExpressを独立したサーバー起動はせず、SuperTest上でExpressのコードを動かします。テストコードからは内蔵されているHTTPクライアント・ライブラリーsuperagentを使ってテストを書きます

テストコードの例

import { beforeEach, afterAll, describe, expect, it } from '@jest/globals'
import request from 'supertest'
import app from '../../src/server'
import { getSidCookie, setupDatabase, cleanupDatabase } from '../helper'

beforeEach(setupDatabase)
afterAll(cleanupDatabase)

describe("Mutation: userLogin(email, password)", () => {

  describe("正しいemail,passwordの場合", () => {
    let res: request.Response

    beforeEach(async() => {
      res = await request(app).post("/graphql").send({
        query: `mutation {
          userLogin(email: "yama@mail.com", password: "test@123") {
            id name email
          }}`
      })
    })

    it("成功し、ユーザー情報が戻る", () => {
      expect(res.status).toBe(200)
      const userLogin = res.body.data.userLogin
      expect(userLogin.email).toBe("yama@mail.com")
      expect(userLogin.name).toBe("ユーザ1")
    })
    it("成功した場合はCookieにSessionIDが入っている", () => {
      expect(res.status).toBe(200)
      expect((getSidCookie(res)).length).toBeGreaterThan(10)
    })
  })

  ・・・・

})

テスティング用データーベース

さて、Apollo GraphQLに入門してみた(おまけ)、リファクタリング・テスティングに書いた時点ではいくつかの点を無視していました。

以前は、Ruby on Railsを使っていましたが、Railsにはテスト駆動開発を行いやすいように、いろいろな機能がRailsやrspec-railsに内蔵されていました。

テスト用データーベース

テスティング用のデーターベースは開発用と同じでもなんとかなるかもしれませんが、テスト専用のデーターベースがあると便利です。docker-composeを使っているのでデータベース名は環境変数でサーバーに渡しているので、テスト時はそれを変更すれば良いので、docker-composeの設定ファイルを複数指定することでテスト実行ができます。

  • テストの実行
docker-compose -f docker-compose.yml -f dc.test.yml up
  • dc.test.yml
version: "3.7"

services:
  server:
    environment:
      - DB_DATABASE=digimagi_test
    command: bash -c "
      ./tools/wait-for-it.sh localhost:5432 -- npm run migrate && npm test"
テストデータ

テストを書くには、テスト開始時にいつも同じテストデータから始められるとテストが書きやすくなります。Railsにはテストデータを簡単に準備できるFixtureが組み込まれています、またfactory_botのようなテストデータ作成ライブラリーもあります。
node.jsにも似たものはあるようですが、今回は専用のSQLを書いて済ましました。

また、データベースの接続ハンドラーはApollo GraphQLサーバーのコードから取得しています。SuperTestはテストコード上でサーバーのコードが動くのでデータベース接続ハンドラーは共有できます。

import app, { db } from '../src/server'

export const setupDatabase = async () => {
  await db.none("BEGIN")

  await db.none("TRUNCATE TABLE session,works,users")
  await db.none("ALTER SEQUENCE users_id_seq RESTART")
  await db.none("ALTER SEQUENCE works_id_seq RESTART")

  await db.none(`INSERT INTO users(name,email,language,password) VALUES('山田',
    'yama@mail.com', '日本語', '*******************************')`)
  await db.none(`INSERT INTO users(name,email,language,password) VALUES('川田',
    'kawada@mail.com', '日本語', '*******************************')`)
  await db.none(`INSERT INTO works(name,user_id,all_data,created_at) VALUES('作品1',
    1, '{"a": 11, "b": 21, "c": {"x": 10, "y": 20}}', '2022-03-17T23:18:00Z')`)
  await db.none(`INSERT INTO works(name,user_id,all_data,created_at) VALUES('作品2',
    1, '{"a": 12, "b": 22, "c": {"x": 10, "y": 20}}', now())`)
  await db.none(`INSERT INTO works(name,user_id,all_data,created_at) VALUES('作品3',
    2, '{"a": 13, "b": 23, "c": {"x": 10, "y": 20}}', now())`)

  await db.none("COMMIT")
}

export const cleanupDatabase = async () => {
  await db.$pool.end()
}

ときどきテストが失敗する

さて、npm test を行うと時々テストが失敗します。しかしテストファイルを1つだけ指定すると成功します。😅

ログを見ていると、複数のテストファイルが同時に実行されているようです。Jestコマンドのドキュメント を調べてみると--maxWorkersというオプションがありました。 デフォルトはCPUコア数の50%なので、私の使っているMacでは3です。
したがって、場合によっては3個のテストファイルが同時に実行されます。同時にテストコードが実行されるとデータベース操作が重なり、思わぬエラーが発生してしまいます。

解決方法は、--maxWorkers=1を指定してテストが複数同時実行されないようにします。テスト時間は長くなりますが、現在のところテスト時間はまったく問題ありません。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ