😆
LocalStackでAWS SDK for Go V2のユニットテスト
やること
前回の記事
前回に引き続き、AWS SDK for Go V2
のユニットテストを行う。
今回はLocalStack
を使って、ローカルでDynamoDB
のテストをしてみる。
準備
今回使用する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を利用する。
公式のサンプルを元に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での操作
(テストには関係ないため、読み飛ばしても構いません。)
LocalStack
をAWS 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
}
}
DynamoDB
のproducts
テーブルに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