😆

LocalStackでAWS SDK for Go V2のユニットテスト

2024/03/06に公開

やること

前回の記事

https://zenn.dev/wac/articles/0c2997f6e1aefc

前回に引き続き、AWS SDK for Go V2のユニットテストを行う。
今回はLocalStackを使って、ローカルでDynamoDBのテストをしてみる。

https://www.localstack.cloud/

準備

今回使用するGoのバージョン:

% go version
go version go1.22.0 darwin/arm64

Dockerのバージョン:

% docker --version
Docker version 25.0.2, build 29cf629

Docker Composeのバージョン:

% docker-compose version
Docker Compose version v2.24.3-desktop.1

AWS CLIのバージョン:

% aws --version
aws-cli/2.13.34 Python/3.11.6 Darwin/23.3.0 exe/x86_64 prompt/off

プロジェクトの初期化:
<github account><repository>は環境に合わせて変更。

% go mod init github.com/<github account>/<repository>
go: creating new go.mod: module github.com/<github account>/<repository>

go.modファイルが作成されている。

Docker Composeの準備

今回はDocker Composeを利用する。

https://docs.localstack.cloud/getting-started/installation/#starting-localstack-with-docker-compose

公式のサンプルを元にdocker-compose.ymlを作成。

version: "3.8"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # external services port range
    environment:
      # LocalStack configuration: https://docs.localstack.cloud/references/configuration/
      - DEBUG=${DEBUG:-0}
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"
  • LOCALSTACK_DOCKER_NAME: Dockerコンテナの名前
  • LOCALSTACK_VOLUME_DIR: Docker volumeをマウントするディレクトリ

は必要に応じて設定できる。
今回は、.envを作成して、コンテナの名前をlocalstack-testに設定する。

LOCALSTACK_DOCKER_NAME=localstack-test

この状態でdocker-compose upをしてコンテナを立ち上げる。

docker-compose up -d

docker psで立ち上げたlocalstack-testがあるかを確認。

% docker ps
CONTAINER ID   IMAGE                   COMMAND                  CREATED          STATUS                    PORTS                                                                    NAMES
672f106fa4e7   localstack/localstack   "docker-entrypoint.sh"   43 seconds ago   Up 43 seconds (healthy)   127.0.0.1:4510-4559->4510-4559/tcp, 127.0.0.1:4566->4566/tcp, 5678/tcp   localstack-test

AWS CLIでの操作

(テストには関係ないため、読み飛ばしても構いません。)

LocalStackAWS CLIで試しに動かしてみる。

LocalStack用のAWS profileの作成:

% aws configure --profile localstack
AWS Access Key ID [None]: test
AWS Secret Access Key [None]: test
Default region name [None]: us-east-1
Default output format [None]:

バケットの作成:

% aws s3 mb s3://test-bucket --endpoint http://localhost:4566 --profile localstack
make_bucket: test-bucket

バケットの一覧:

% aws s3 ls --endpoint http://localhost:4566 --profile localstack
2024-03-06 15:56:17 test-bucket

バケットの削除

% aws s3 rb s3://test-bucket --endpoint http://localhost:4566 --profile localstack
remove_bucket: test-bucket

アプリケーションコードの作成

main.goを作成して、下記のコードを追加する。

package main

import (
    "context"
    "log/slog"
    "os"
    
    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb"
)

type Product struct {
    Id   string `dynamodbav:"id"`
    Name string `dynamodbav:"name"`
}

type DynamoDBRepository struct {
    client    *dynamodb.Client
    tableName string
}

func (r *DynamoDBRepository) Create(product Product) error {
    av, err := attributevalue.MarshalMap(product)
    if err != nil {
        return err
    }
    
    input := &dynamodb.PutItemInput{
        Item:      av,
        TableName: &r.tableName,
    }
    
    _, err = r.client.PutItem(context.Background(), input)
    if err != nil {
        return err
    }
    
    return nil
}

func NewDynamoDBRepository(cfg aws.Config, tableName string) (*DynamoDBRepository, error) {
    client := dynamodb.NewFromConfig(cfg)
    return &DynamoDBRepository{client: client, tableName: tableName}, nil
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    tableName := "products"
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        logger.Error("failed to load configuration", "error", err)
        return
    }
    repo, err := NewDynamoDBRepository(cfg, tableName)
    if err != nil {
        logger.Error("failed to create repository", "error", err)
        return
    }
    
    product := Product{
        Id:   "1",
        Name: "Product 1",
    }
    
    err = repo.Create(product)
    if err != nil {
        logger.Error("failed to create product", "error", err)
        return
    }
}

DynamoDBproductsテーブルにProductを書き込むコード。

テストコードの作成

main_test.goを作成して、下記のコードを追加する。

package main

import (
    "context"
    "os"
    "testing"
    
    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
    "github.com/stretchr/testify/assert"
)

const testTableName = "test_products"

func setupTestTable(t *testing.T, tableName string) *DynamoDBRepository {
    // AWS認証情報を設定(テスト用のダミー値を設定)
    t.Setenv("AWS_ACCESS_KEY_ID", "test")
    t.Setenv("AWS_SECRET_ACCESS_KEY", "test")
    // LocalStackのエンドポイントを設定
    t.Setenv("AWS_ENDPOINT_URL", "http://localhost:4566")
    
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        t.Fatal(err)
    }
    
    repo, err := NewDynamoDBRepository(cfg, tableName)
    if err != nil {
        t.Fatal(err)
    }
    
    // テスト用テーブルをLocalstack上に作成
    createTableInput := &dynamodb.CreateTableInput{
        AttributeDefinitions: []types.AttributeDefinition{
            {
                AttributeName: aws.String("id"),
                AttributeType: types.ScalarAttributeTypeS,
            },
        },
        KeySchema: []types.KeySchemaElement{
            {
                AttributeName: aws.String("id"),
                KeyType:       types.KeyTypeHash,
            },
        },
        ProvisionedThroughput: &types.ProvisionedThroughput{
            ReadCapacityUnits:  aws.Int64(10),
            WriteCapacityUnits: aws.Int64(10),
        },
        TableName: &repo.tableName,
    }
    
    _, err = repo.client.CreateTable(context.TODO(), createTableInput)
    if err != nil {
        t.Fatal(err)
    }
    
    return repo
}

func cleanupTestTable(t *testing.T, repo *DynamoDBRepository) {
    // テスト用テーブルの削除
    deleteTableInput := &dynamodb.DeleteTableInput{
        TableName: &repo.tableName,
    }
    
    _, err := repo.client.DeleteTable(context.TODO(), deleteTableInput)
    if err != nil {
        t.Fatal(err)
    }
}

func TestDynamoDBRepository_Create(t *testing.T) {
    tableName := testTableName
    repo := setupTestTable(t, tableName)
    defer cleanupTestTable(t, repo)
    
    // テスト用のProductを作成して保存
    product := Product{
        Id:   "1",
        Name: "Test Product",
    }
    
    err := repo.Create(product)
    assert.NoError(t, err)
    
    // 作成したProductを取得して、保存されているか確認
    getItemInput := &dynamodb.GetItemInput{
        TableName: &repo.tableName,
        Key: map[string]types.AttributeValue{
            "id": &types.AttributeValueMemberS{Value: product.Id},
        },
    }
    
    output, err := repo.client.GetItem(context.TODO(), getItemInput)
    assert.NoError(t, err)
    assert.NotNil(t, output.Item)
    
    // 取得したProductが保存したProductと一致するか確認
    var retrievedProduct Product
    err = attributevalue.UnmarshalMap(output.Item, &retrievedProduct)
    assert.NoError(t, err)
    assert.Equal(t, product, retrievedProduct)
}

func TestMain(m *testing.M) {
    exitCode := m.Run()
    os.Exit(exitCode)
}

テストの実行

テストOK

% go test ./...
ok      github.com/<github account>/<repository>      0.423s

まとめ

AWS_ENDPOINT_URLを設定するだけで、AWS SDK for Go V2からLocalStackを簡単に利用できる。
次回につづく。

Discussion