外部APIとの連携を効率化するコード生成とテスト

ogp

1. はじめに

enechain eClearデスク兼GXデスクのバックエンドエンジニア @eji です。

サービスを開発する上で、外部APIとの連携は避けて通れない課題です。 eClearデスクでも様々な外部APIと連携する機会がありました。

今回は、外部APIとの連携を効率化するために取り組んだ、コード生成とテスト方法を紹介します。

対象読者

  • 外部APIを利用するWebアプリを開発しているエンジニア
  • フロントエンドとバックエンドの担当が分かれている開発チーム
  • 手戻りを減らし、効率的にAPI開発を進めたい人
  • OpenAPI / OAuth2 / 自動テストに興味がある人

前提条件

  • OpenAPI の基本的な概念を知っている(詳細な説明は省略)
  • OAuth2 認可コードフローの概要を理解している
  • Go で API を開発していることを想定

2. 課題

直近の開発は以下のような状況でした。

  • API仕様書がExcelやPDFなどの非構造化フォーマットで提供されている
  • フロントエンドとバックエンドの担当が分かれている

仕様書がExcelやPDFで提供されることは、API連携の開発においてよくあるケースです。 また、フロントエンドとバックエンドで担当チームが分かれることもよくあるケースだと思います。

上記のようなケースでは、次のような課題が良く発生すると思います。

  • API仕様書がExcelやPDFなどの非構造化フォーマットで提供されている
    • → 仕様の解釈の違いが発生しやすい。
    • → これにより、受け入れテストの段階で実装の不具合が見つかり、手戻りが発生する
  • フロントエンドとバックエンドの担当が分かれている
    • → バックエンドの実装の遅延がフロントエンドに影響する

3. 取り組んだこと

① OpenAPIでAPI仕様書を作成する

最初に取り組んだのは、Excel形式のAPI仕様書からOpenAPI形式の仕様書を作成することでした。

OpenAPIでAPI仕様書を作成することで、仕様的に曖昧な部分を洗い出すことができます。 この段階で連携先と仕様を擦り合わせることにより、実装時の手戻りを減らすことができます。

② OpenAPI形式の仕様書を元にクライアントとサーバーのコードを自動生成

次に、OpenAPI形式の仕様書を元に、クライアントとサーバーのコードを自動生成しました。

コードの自動生成は github.com/oapi-codegen/oapi-codegen/v2 を利用しました。

コード生成用の設定は次の通りです。

# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json
package: xyz
output: pkg/client/xyz/xyz.gen.go
generate:
  models: true # モデルを生成
  client: true # クライアントコードを生成
  strict-server: true # サーバーコードを生成
  std-http-server: true # net/httpを利用したサーバーコードを生成

外部APIを利用するだけなのでクライアントコードだけで十分なのですが、「テストで モックサーバーとして利用する」ためにサーバーコードも生成しました。

ここで、モックサーバーや後述するフェイクサーバーは、次のような違いがあります。

項目 モックサーバー フェイクサーバー
外部API通信 なし。テスト用のレスポンスを返す なし。用意したデータを返す
レスポンス 固定or動的(テスト次第) 動的(本番っぽく再現)
リクエスト検証 ◯(内容や回数をチェック) ×(検証しない)
主な目的 テストの制御と検証 本番環境の動作を模倣
使う場面 CI/CD、リトライやエラー処理のテスト ユーザー別データ返却、認証フローの再現

③ フェイクサーバーを作成し、フロントエンドの開発を並行化

今回の連携先のサービスは認証プロバイダも兼ねており、OAuth2の認可コードフローを実装する必要がありました。

  • フロントエンドは、バックエンド経由で認証プロバイダの認証ページにリダイレクトします。
  • 認証後、認証プロバイダは 登録済みのリダイレクトURI認可コード を付与してブラウザをリダイレクトします。
  • フロントエンドはこの認可コードをバックエンドに送信し、バックエンドが認証プロバイダから アクセストークン を取得します。

ogp

このようなフローを実現するために、フロントエンドとバックエンド双方の実装、認可プロバイダへの接続が必要です。

フロントエンドはバックエンドの実装に依存しており、バックエンド実装の遅延がフロントエンド実装の遅延につながります。 また、認可プロバイダへの接続準備が整っていない場合は、フロントエンドの開発が進められません。

そこで、バックエンドと認証プロバイダのそれぞれのフェイクサーバーを作成して、フロントエンドの開発を進めることにしました。

ogp

上記の対応により、フロントエンドはバックエンドの実装を待たずに、かつ、認可サーバーの準備が整う前にも、 フェイクサーバーを使って開発を進めることができました。

④ モックサーバーを作成し、バックエンドの開発とテストを自動化

バックエンドの開発では、生成したサーバーコードをモックサーバーとして活用して実装を進めました。 モックサーバーを使うことで、外部APIと通信モジュールをモックへ差し替えずに開発を進められます。

そのため、実際に外部APIと連携する前に通信モジュールのテストが可能になります。 また、モックサーバーにすることで、リクエストの検証や呼び出し回数もテストできます。

以下は、モックサーバーを作成する関数の一例です(説明のため、詳細は省いています)。

func StartXyzMockServer(t *testing.T, ssi xyz.StrictServerInterface) int {
    // 空いているポートを取得
    listener, err := net.Listen("tcp", "localhost:0")
    require.NoError(t, err)

    // テスト終了時にポートを閉じる
    t.Cleanup(listener.Close)

    // モックサーバーを起動
    si := xyz.NewStrictHandler(ssi, nil)
    s := &http.Server{
        Handler:          xyz.Handler(si),
        ReadHeaderTimeout: 5 * time.Second,
    }

    // ポート番号を返す
    port := listener.Addr().(*net.TCPAddr).Port
    return port
}

func TestXyz(t *testing.T) {
    t.Run("test xyz", func(t *testing.T) {
        // モックを初期化
        ssiMock := mocks.NewMockStrictServerInterface(t)
        ssiMock.EXPECT()... 
        // モックサーバーを起動
        port := StartXyzMockServer(t, ssiMock)

        // モックサーバーと通信するように設定した状態で、テスト対象のサーバーを起動
        conn := e2e.StartGrpcServer(ctx, t, e2eopt.WithXyzServer(port))
        client := xyzsrv.NewXyzServiceClient(conn)
        // テスト対象のエンドポイントの呼び出し
        // 内部で外部APIと通信する(実際にはモックサーバーと通信)
        res, err := client.GetAuthURL(ctx, &xyzsrv.XyzRequest{...})
        // 結果の検証
        require.NoError(t, err)
        require.Equal(t, "https://example.com", res.URL)    
    })
}

モックサーバーを使ってテストすることにより、次のようなメリットが得られました。

  • 外部APIとの通信モジュール部分も含めたテスト
  • 実際に外部APIと接続した際のエラーを再現可能
  • リクエストの検証や呼び出し回数のテスト
  • 受け入れテストで、実装の修正がほぼ不要になる

4. 成果

  • 開発の初期でAPI仕様をOpenAPI形式にすることで、認識ミスによる手戻りを予防できた
  • フェイクサーバーの利用により、フロントエンドとバックエンドの並行開発が可能になった
  • 受け入れテスト時に実装の修正がほぼ不要になった

5. まとめ

  • API連携では、仕様の認識ズレが手戻りの原因になりやすい
  • OpenAPI による API 仕様管理 + コード生成を活用すると、認識ズレを防げる
  • フェイクサーバーを活用すれば、フロントエンドとバックエンドの並行開発が可能
  • モックサーバーを使ったテストの自動化により、外部APIの受け入れテストもスムーズに
  • この手法を導入することで、API 開発の効率が大幅に向上する

enechainでは、事業拡大のために随時仲間を募集しています。興味がある方はぜひお声がけください!

herp.careers

herp.careers