STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

Goのテストで GraphQL APIサーバのE2Eテストを書く方法

はじめに

こんにちは、STORESの高田です。

STORES には Go で実装した GraphQL API サーバがあり、そのプロジェクトでは Go のテスト内で E2E テストを行っています。今回はそのテスト方法についてご紹介します。

実装例

今回の E2E テストは CI でも実行したいため、再現性のある安定したテストであることが求められます。

少し工夫する必要がありますが、テスト対象となるサーバのポート確保時に、エニーポートを指定して動的にポートを割り当てることで安定したテストを実現できます。

以下のコードのように、 TestMain 内での net.Listen("tcp", "localhost:0") にて動的にポートを割り当てます。

var client AppGraphQLClient

func TestMain(m *testing.M) {
    // データベースなどの初期化
    ...

    ln, err := net.Listen("tcp", "localhost:0") // 動的にポートを割り当てる
    if err != nil {
        log.Fatal(err)
    }
    addrPort, err := netip.ParseAddrPort(ln.Addr().String())
    if err != nil {
        log.Fatal(err)
    }

    httpCli := &http.Client{}
    client = NewClient(httpCli, fmt.Sprintf("http://localhost:%d/graphql", addrPort.Port()), nil)

    srv := &http.Server{
        Handler:           di.InitializeHandler(),
        ReadHeaderTimeout: 10 * time.Second,
    }
    ch := make(chan struct{})
    go func() {
        if err := srv.Serve(ln); err != http.ErrServerClosed {
            log.Fatal(err)
        }
        close(ch)
        // データベースなどの teardown
        ...
    }()

    m.Run()

    if err := srv.Shutdown(context.Background()); err != nil {
        log.Fatal(err)
    }

    <-ch
}

API クライアントの生成には github.com/Yamashou/gqlgenc を使用しており、client 変数経由で query や mutation を呼び出してテストを実施します。E2E テストは以下のテストコードのように実装しています。

func TestE2EMe(t *testing.T) {
    // データベースなどの初期化
    ...

    want := &Me{
        Me: Me_Me{
            UserID: "10000000-0000-0000-0000-000000000000",
        },
    }

    got, err := client.Me(context.Background(), func(ctx context.Context, req *http.Request, gqlInfo *clientv2.GQLRequestInfo, res interface{}, next clientv2.RequestInterceptorFunc) error {
        req.Header.Set("X-User-Id", "10000000-0000-0000-0000-000000000000")
        req.Header.Set("X-Scopes", "openid email offline_access")
        return next(ctx, req, gqlInfo, res)
    })
    if err != nil {
        t.Fatal(err)
    }

    if diff := cmp.Diff(want, got); diff != "" {
        t.Errorf("diff (-want +got):\n%s", diff)
    }
}

今回はテストの実装方針として TestMain で初期化する方針にしていますが、初期化処理は局所化することも可能なので、プロジェクトにとって都合の良い方針で進められると良いと思います。また、RESTful API の場合にも応用可能です。事例として参考にしてもらえればと思います.