🆒

GolangとAzure API Managementを利用したテスト戦略

2023/12/24に公開

この記事はJij Inc. Advent Calendar 2023の24日目の記事です。
はじめまして、株式会社Jij の寺町です。


導入

本記事では、GolangとAzure API Managementを組み合わせた開発プロセスと、Azure SDKのFake機能を用いたテスト戦略に焦点を当てます。

GolangとAzure API Managementの概要

Golangは効率的なコンパイル、簡潔な構文、並行処理サポートにより、クラウドネイティブアプリケーション開発に適しています。Azure API ManagementはAPIを一元的に管理し、セキュリティやルーティングの管理を可能にします。

実装の流れ

SDKバージョンv2.1.0を前提に、Azure API ManagementのSDKの基本操作を説明します。

Golangのセットアップ

Goの設定が完了していることを前提とし、Azureへのアクセスは環境変数を介して行います。必要なパッケージは以下の通りです。

go get "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2"
go get "github.com/Azure/azure-sdk-for-go/sdk/azidentity"

実装

API ManagementからGroupを取得するコードを紹介し、重要な部分を説明します。環境変数の設定から、Azure SDK for Goを用いた認証情報の作成、Groupの取得までをカバーします。

注意点
"fake-group" はサンプルのための名称であり、実際のGroup名を使用する必要があります。

package main

import (
	"context"
	"fmt"
	"os"

	identity "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	apim "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2"
)

func main() {
	// Get the environment variables
	clientID := os.Getenv("AZURE_CLIENT_ID")
	clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
	tenantID := os.Getenv("AZURE_TENANT_ID")
	subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID")
	serviceName := os.Getenv("AZURE_APIM_NAME")
	resourceGroupName := os.Getenv("AZURE_RESOURCE_GROUP")
	ctx := context.Background()
	// Create Credentials and create a new Authorizer from Azure Go SDK
	creds, err := identity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
	if err != nil {
		return
	}

	// Create a new Clietn Factory
	factory, err := apim.NewClientFactory(subscriptionID, creds, nil)
	if err != nil {
		return
	}

	client := factory.NewGroupClient()
	options := &apim.GroupClientListByServiceOptions{}

	pager := client.NewListByServicePager(resourceGroupName, serviceName, options)
	for pager.More() {
		resp, err := pager.NextPage(ctx)
		if err != nil {
			break
		}
		for _, groupContract := range resp.GroupCollection.Value {
			fmt.Println(*groupContract.Name)
		}
	}

	// Get group
	group, err := client.Get(ctx, resourceGroupName, serviceName, "fake-group", nil)
	if err != nil {
		return
	}
	fmt.Println(*group.Name)
}

Fakeの利用

Azure SDK for GoのFake機能を使用して、実際にAPI Managementに接続することなく操作を模擬する方法は、ユニットテストにおいて非常に有効です。このアプローチでは、実際のAPI Managementサービスとの通信を行わずに、APIの動作をテストできます。この方法を用いることで、実際のリソースへの依存を避けながら、API管理機能のユニットテストをより効率的かつ正確に行うことが可能になります。これにより、開発プロセスがより迅速かつ柔軟になり、品質の高いコードの保守が容易になります。
実際のコードは以下のようになります。

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
	azfake "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
	apim "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2/fake"
)

func main() {
	// Get the environment variables
	serviceName := os.Getenv("AZURE_APIM_NAME")
	resourceGroupName := os.Getenv("AZURE_RESOURCE_GROUP")
	ctx := context.Background()

	// FakeClientに変更
	client := NewGroupFakeClient()
	options := &apim.GroupClientListByServiceOptions{}

	pager := client.NewListByServicePager(resourceGroupName, serviceName, options)
	for pager.More() {
		resp, err := pager.NextPage(ctx)
		if err != nil {
			break
		}
		for _, groupContract := range resp.GroupCollection.Value {
			fmt.Println(*groupContract.Name)
		}
	}

	// Get group
	group, err := client.Get(ctx, resourceGroupName, serviceName, "fake-group", nil)
	if err != nil {
		return
	}
	fmt.Println(*group.Name)
	_, err = client.Get(ctx, resourceGroupName, serviceName, "none" , nil)
	if err != nil {
		return
	}

}

func NewGroupFakeClient() *apim.GroupClient {
	groupServer := &fake.GroupServer{
		CreateOrUpdate: func(ctx context.Context, resourceGroupName string, serviceName string, groupID string, parameters apim.GroupCreateParameters, options *apim.GroupClientCreateOrUpdateOptions) (resp azfake.Responder[apim.GroupClientCreateOrUpdateResponse], errResp azfake.ErrorResponder) {
			if groupID == "not-found" {
				errResp.SetResponseError(404, "FakeNotFound")
				return
			}
			groupResp := apim.GroupClientCreateOrUpdateResponse{
				GroupContract: apim.GroupContract{
					ID: to.Ptr(fmt.Sprintf("/subscriptions/subid/resourceGroups/%s/providers/Microsoft.ApiManagement/service/%s/groups/%s", resourceGroupName, serviceName, groupID)),
					Name: to.Ptr(groupID + "-name"),
				},
			}
			resp.SetResponse(200, groupResp, nil)
			return
		},
		Delete: func(ctx context.Context, resourceGroupName string, serviceName string, groupID string, ifMatch string, options *apim.GroupClientDeleteOptions) (resp azfake.Responder[apim.GroupClientDeleteResponse], errResp azfake.ErrorResponder) {
			if groupID == "not-found" {
				errResp.SetResponseError(404, "FakeNotFound")
				return 
			}
			groupResp := apim.GroupClientDeleteResponse{}

			resp.SetResponse(200, groupResp, nil)
			return
		},
		Get: func(ctx context.Context, resourceGroupName string, serviceName string, groupID string, options *apim.GroupClientGetOptions) (resp azfake.Responder[apim.GroupClientGetResponse], errResp azfake.ErrorResponder){
			if groupID == "not-found" {
				errResp.SetResponseError(404, "FakeNotFound")
				return
			}
			groupResp := apim.GroupClientGetResponse{}
			groupResp.ID = to.Ptr(fmt.Sprintf("/subscriptions/subid/resourceGroups/%s/providers/Microsoft.ApiManagement/service/%s/groups/%s", resourceGroupName, serviceName, groupID))
			groupResp.Name = to.Ptr(groupID + "-name")
			groupResp.ETag = to.Ptr("etag")
			resp.SetResponse(200, groupResp, nil)
			return
		},
		NewListByServicePager: func(resourceGroupName, serviceName string, options *apim.GroupClientListByServiceOptions) (resp azfake.PagerResponder[apim.GroupClientListByServiceResponse]) {
			if serviceName == "not-found" {
				resp.AddResponseError(404, "FakeNotFound")
				return
			}
			page1 := apim.GroupClientListByServiceResponse{
				GroupCollection: apim.GroupCollection{
					Value: []*apim.GroupContract{
						{
							Name: to.Ptr("g1"),
							ID: to.Ptr(fmt.Sprintf("/subscriptions/subid/resourceGroups/%s/providers/Microsoft.ApiManagement/service/%s/groups/g1", resourceGroupName, serviceName)),
						},
						{
							Name: to.Ptr("g2"),
							ID: to.Ptr(fmt.Sprintf("/subscriptions/subid/resourceGroups/%s/providers/Microsoft.ApiManagement/service/%s/groups/g2", resourceGroupName, serviceName)),
						},
					},
					Count: to.Ptr(int64(2)),
				},
			}
			page2 := apim.GroupClientListByServiceResponse{
				GroupCollection: apim.GroupCollection{
					Value: []*apim.GroupContract{
						{
							Name: to.Ptr("g3"),
							ID: to.Ptr(fmt.Sprintf("/subscriptions/subid/resourceGroups/%s/providers/Microsoft.ApiManagement/service/%s/groups/g3", resourceGroupName, serviceName)),
						},
						{
							Name: to.Ptr("g4"),
							ID: to.Ptr(fmt.Sprintf("/subscriptions/subid/resourceGroups/%s/providers/Microsoft.ApiManagement/service/%s/groups/g4", resourceGroupName, serviceName)),
						},
					},
					Count: to.Ptr(int64(2)),
				},
			}
			resp.AddPage(200, page1, nil)
			resp.AddPage(200, page2, nil)

			return
		},
	}
	// APIServerTransportのインスタンスを作成
	transport := fake.NewGroupServerTransport(groupServer)

	// APIClientをインスタンス化して、トランスポートを設定
	client,err := apim.NewGroupClient("fake-subscription",
		&azfake.TokenCredential{},&arm.ClientOptions{
			ClientOptions: azcore.ClientOptions{Transport: transport}})
	if err != nil {
		fmt.Println("FakeGroupClient:", err)
		return nil
	}
	return client

}

実行結果は以下になります。

$ go run main.go
g1
g2
g3
g4
fake-group-name

結論

Golangのシンプルな記述性は、多くの開発者にとって書きやすい印象を与えます。特に、Azure SDKのFake機能を活用することで、実際のリソースの振る舞いを効果的に模倣でき、ユニットテストの容易さが向上します。

まとめ

本記事では、GolangとAzure API Managementを組み合わせた開発とテストの実践的な方法を紹介しました。この組み合わせにより、APIの管理とテストを効率的かつ効果的に行うことが可能です。

最後に

\Rustエンジニア・数理最適化エンジニア募集中!/
株式会社Jijでは、数学や物理学のバックグラウンドを活かし、量子計算と数理最適化のフロンティアで活躍するRustエンジニア、数理最適化エンジニアを募集しています!
詳細は下記のリンクからご覧ください。皆さんのご応募をお待ちしております!
Rustエンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/51062
数理最適化エンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/75132

Discussion