1. はじめに

AWSを利用したアプリケーションを開発する際、コードが正しく動作するかどうかをテストすることは非常に重要です。しかし、実際のAWSクラウド環境に接続してテストを行うと、時間やコストがかかるだけでなく、リソースに予期せぬ変更を加えてしまうリスクもあります。

そこで役立つのが「モック」という手法です。モックとは、本来のサービスやオブジェクトの動作を模倣したテスト用のダミーオブジェクトを使用する方法です。これにより、実際のAWSサービスに接続せずにコードを検証でき、開発効率を大幅に向上させることができます。

本記事では、Java で広く使われているモックライブラリであるMockito を使用して、AWS SDK を利用したテスト手法を解説します。具体的には、DynamoDB やS3 を例に挙げて、モックを使ったデータの保存・取得操作のテストの書き方を説明します。

2. モックの活用

2.1 モックとは

モックとは、本来のサービスやオブジェクトの動作を模倣したテスト用のダミーオブジェクトです。テスト時に外部システムや複雑な依存関係を持つオブジェクトの振る舞いをシミュレートするために使用されます。AWS SDK の動作を模倣するダミーオブジェクトを利用すると、ネットワーク接続や実際のAWSリソースを使用せずに、AWSサービスを利用するコードのテストが可能になります。

2.2 モックを利用する利点

モックを利用することで、以下のような利点が得られます。

  • コスト削減:AWSリソースを実際に使用しないため、テスト時の費用を抑えられます
  • 速度向上:ローカル環境でテストが完結するため、テストの実行が速くなります
  • 安定性:ネットワークの状態やAWSサービスの不安定さに影響されず、安定してテストを実行できます

2.3 Mockito とは

Mockito は、Javaのユニットテストで広く使用されているモックフレームワークです。テスト対象のコードが依存するオブジェクトや外部サービスの振る舞いを模倣し、制御することができます。
Mockito の主な特徴は以下の通りです。

  • 簡単な使用方法:直感的なAPIを提供しており、モックオブジェクトの作成や振る舞いの定義が容易です
  • 検証機能:モックオブジェクトのメソッド呼び出しを検証し、期待通りの相互作用が行われたかを確認できます
  • JUnit統合:JUnitと簡単に統合でき、テストコードの可読性と保守性を向上させます

Mockito を使用することで、外部依存性(例えばAWS SDKなど)を持つコードの単体テストが容易になり、テストの品質と開発効率を向上させることができます。

3. テスト環境の準備

3.1 必要なツールとライブラリ

  • JVM:Amazon Corretto 17 を使用します
  • ビルドツール:Gradle を使用します
  • テストフレームワーク:JUnit 5 を使用します
  • モックライブラリ:Mockito を使用します

実際に検証した際の環境は以下となります。

  • Gradle:8.2
  • JVM:17.0.10 (Amazon.com Inc. 17.0.10+7-LTS)
  • OS:Windows 10 10.0 amd64

3.2 Gradleの設定

build.gradle ファイルに以下の依存関係を追加します。バージョンは最新の安定版を使用してください。

dependencies {
  // AWS SDK for DynamoDB
  implementation 'software.amazon.awssdk:dynamodb:2.28.1'
  // AWS SDK for S3
  implementation 'software.amazon.awssdk:s3:2.28.1'

  // JUnit 5
  testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'

  // Mockito
  testImplementation 'org.mockito:mockito-core:5.13.0'
  testImplementation 'org.mockito:mockito-junit-jupiter:5.13.0'
}

3.3 テストの実行設定

Gradle でJUnit5 のテストを実行するため、build.gradleに以下を追加します。

test {
    useJUnitPlatform()
}

4. DynamoDBの操作をモック化する

4.1 Amazon DynamoDB とは

Amazon DynamoDBは、AWSが提供するフルマネージドのNoSQLデータベースサービスです。高いスケーラビリティと低レイテンシーでデータの読み書きが可能です。

4.2 サンプルコードの作成

DynamoDB にデータを保存・取得するクラス「DynamoDbService」を作成します。
DynamoDbClient にてDynamoDB を操作するため、このオブジェクトがモックする対象となります。
モックオブジェクトを注入するためのコンストラクタ、アプリで実際に使うためにDynamoDbClient をnew するコンストラクタの2種類用意します。

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;

import java.util.Map;

public class DynamoDbService {
  private final DynamoDbClient dynamoDbClient;

  /**
  * モック用コンストラクタ.
  * @param dynamoDbClient DynamoDBクライアント
  */
  public DynamoDbService(DynamoDbClient dynamoDbClient) {
    this.dynamoDbClient = dynamoDbClient;
  }

  /**
  * コンストラクタ.
  * @param region AWSリージョン
  */
  public DynamoDbService(Region region) {
    this(DynamoDbClient.builder().region(region).build());
  }

  /**
  * アイテムを保存します.
  * @param tableName テーブル名
  * @param item 保存するアイテム
  */
  public void putItem(String tableName, Map<String, AttributeValue> item) {
    PutItemRequest request = PutItemRequest.builder()
        .tableName(tableName)
        .item(item)
        .build();
    dynamoDbClient.putItem(request);
  }

  /**
  * アイテムを取得します.
  * @param tableName テーブル名
  * @param key 取得するアイテムのキー
  * @return 取得したアイテム
  */
  public Map<String, AttributeValue> getItem(String tableName, Map<String, AttributeValue> key) {
    GetItemRequest request = GetItemRequest.builder()
        .tableName(tableName)
        .key(key)
        .build();
    GetItemResponse response = dynamoDbClient.getItem(request);
    return response.item();
  }
}

4.3 テストの作成

Mockito のアノテーションを使用して、モックオブジェクトを注入します。
JUnit のアサーションを用いて意図した状態となっているかテストします。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class DynamoDbServiceTest {

  @Mock
  private DynamoDbClient dynamoDbClient;

  @InjectMocks
  private DynamoDbService dynamoDbService;

  @Test
  void データを保存できること() {
    /* 準備 */
    String tableName = "TestTable";
    Map<String, AttributeValue> item = new HashMap<>();
    item.put("id", AttributeValue.builder().s("123").build());
    item.put("name", AttributeValue.builder().s("テストデータ").build());

    /* 実行 */
    dynamoDbService.putItem(tableName, item);

    /* 検証 */
    PutItemRequest expectedRequest = PutItemRequest.builder()
        .tableName(tableName)
        .item(item)
        .build();

    // verifyを使用して、期待するリクエストでputItemが呼ばれたか確認
    verify(dynamoDbClient).putItem(expectedRequest);
  }

  @Test
  void データを取得できること() {
    /* 準備 */
    String tableName = "TestTable";
    Map<String, AttributeValue> key = new HashMap<>();
    key.put("id", AttributeValue.builder().s("123").build());

    Map<String, AttributeValue> expectedItem = new HashMap<>();
    expectedItem.put("id", AttributeValue.builder().s("123").build());
    expectedItem.put("name", AttributeValue.builder().s("テストデータ").build());

    GetItemResponse mockResponse = GetItemResponse.builder()
        .item(expectedItem)
        .build();

    // whenを使用して、getItemメソッドが呼ばれたときにmockResponseを返すように設定
    when(dynamoDbClient.getItem(any(GetItemRequest.class))).thenReturn(mockResponse);

    /* 実行 */
    Map<String, AttributeValue> actualItem = dynamoDbService.getItem(tableName, key);

    /* 検証 */
    GetItemRequest expectedRequest = GetItemRequest.builder()
        .tableName(tableName)
        .key(key)
        .build();

    // verifyを使用して、期待するリクエストでgetItemが呼ばれたか確認
    verify(dynamoDbClient).getItem(expectedRequest);

    // 取得したアイテムが期待通りか確認
    assertEquals(expectedItem, actualItem);
  }
}

4.4 解説

  • コンストラクタの分離:
    モック用と実際に使用するためのコンストラクタを分けています。モック用コンストラクタではDynamoDbClient を直接受け取り、実際のコンストラクタではRegion を受け取ってクライアントを構築します。
  • モックの設定:
    • @Mock@InjectMocks を使用して、モックオブジェクトを注入します。
    • 戻り値を設定する必要のないメソッドの場合、モックオブジェクトを注入するだけで他の設定は要りません。
    • 戻り値を設定したい場合、when を使用してメソッドの呼び出し時にモックのレスポンスを返すように設定します。
  • 検証:
    • verify を使用して、期待するリクエストでAWS SDK のメソッドが呼ばれたか確認します。
    • assertEquals で戻り値が期待通りであることを確認します。

5. S3 の操作をモック化する

5.1 Amazon S3 とは

Amazon S3 は、AWS が提供するオブジェクトストレージサービスです。大容量のデータを安全かつ高い可用性で保存できます。

5.2 サンプルコードの作成

S3にファイルをアップロード・ダウンロードするクラス「S3Service」を作成します。
S3Client にてS3 を操作するため、このオブジェクトがモックする対象となります。
モックオブジェクトを注入するためのコンストラクタ、アプリで実際に使うためにS3Client をnew するコンストラクタの2種類用意します。

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

public class S3Service {
  private final S3Client s3Client;

  /**
  * モック用コンストラクタ.
  * @param s3Client S3クライアント
  */
  public S3Service(S3Client s3Client) {
    this.s3Client = s3Client;
  }

  /**
  * コンストラクタ.
  * @param region AWSリージョン
  */
  public S3Service(Region region) {
    this(S3Client.builder().region(region).build());
  }

  /**
  * データをS3へアップロードします.
  * @param bucketName バケット名
  * @param key オブジェクトキー
  * @param data アップロードするデータ
  */
  public void uploadData(String bucketName, String key, byte[] data) {
    PutObjectRequest request = PutObjectRequest.builder()
        .bucket(bucketName)
        .key(key)
        .build();
    s3Client.putObject(request, RequestBody.fromBytes(data));
  }

  /**
  * ファイルをS3からダウンロードし、byte配列として返します.
  * @param bucketName バケット名
  * @param key オブジェクトキー
  * @return ダウンロードしたファイルのbyte配列
  */
  public byte[] downloadFile(String bucketName, String key) {
    GetObjectRequest request = GetObjectRequest.builder()
        .bucket(bucketName)
        .key(key)
        .build();
    return s3Client.getObject(request, ResponseTransformer.toBytes()).asByteArray();
  }
}

5.3 テストの作成

Mockito のアノテーションを使用して、モックオブジェクトを注入します。
JUnit のアサーションを用いて意図した状態となっているかテストします。
さらに、例外が発生した場合のテストも追加します。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class S3ServiceTest {

  @Mock
  private S3Client s3Client;

  @InjectMocks
  private S3Service s3Service;

  @Test
  void ファイルをアップロードできること() {
    /* 準備 */
    String bucketName = "test-bucket";
    String key = "test.txt";
    byte[] data = "テストデータ".getBytes();

    /* 実行 */
    s3Service.uploadData(bucketName, key, data);

    /* 検証 */
    PutObjectRequest expectedRequest = PutObjectRequest.builder()
        .bucket(bucketName)
        .key(key)
        .build();

    // verifyを使用して、期待するリクエストでputObjectが呼ばれたか確認
    verify(s3Client).putObject(eq(expectedRequest), any(RequestBody.class));
  }

  @Test
  void ファイルをダウンロードできること() {
    /* 準備 */
    String bucketName = "test-bucket";
    String key = "test.txt";
    byte[] expectedData = "テストデータ".getBytes();

    // モックのレスポンスを作成
    ResponseBytes<GetObjectResponse> mockResponse = ResponseBytes.fromByteArray(GetObjectResponse.builder().build(), expectedData);

    // whenを使用して、getObjectメソッドが呼ばれたときにmockResponseを返すように設定
    when(s3Client.getObject(any(GetObjectRequest.class), ArgumentMatchers.<ResponseTransformer<GetObjectResponse, ResponseBytes<GetObjectResponse>>>any()))
        .thenReturn(mockResponse);

    /* 実行 */
    byte[] actualData = s3Service.downloadFile(bucketName, key);

    /* 検証 */
    GetObjectRequest expectedRequest = GetObjectRequest.builder()
        .bucket(bucketName)
        .key(key)
        .build();

    // verifyを使用して、期待するリクエストでgetObjectが呼ばれたか確認
    verify(s3Client).getObject(eq(expectedRequest), ArgumentMatchers.<ResponseTransformer<GetObjectResponse, ResponseBytes<GetObjectResponse>>>any());

    // ダウンロードしたデータが期待通りか確認
    assertArrayEquals(expectedData, actualData);
  }

  @Test
  void アップロード時に例外が発生した場合のテスト() {
    /* 準備 */
    // doThrowを使用して、putObjectメソッドが呼ばれたときに例外をスローするように設定
    doThrow(SdkClientException.create("エラー"))
        .when(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));

    /* 実行と検証 */
    assertThrows(SdkClientException.class, () -> {
      s3Service.uploadData("bucket", "key", "テストデータ".getBytes());
    });
  }
}

5.4 解説

DynamoDB の例に加え、例外が発生した際のテストケースを追加しています。
doThrow を利用してputObject を実行した際にSdkClientException が発生するように設定しています。
assertThrows を利用して、実際にs3Service.uploadData を実行した際にSdkClientException が発生することを確認しています。

6. モックを使用する際の注意点

モックを利用することで効率的なテストが可能になりますが、正確かつ信頼性の高いテストを行うためにはいくつかの注意点があります。ここでは、具体例を挙げながら注意すべきポイントを解説します。

6.1 SDK の仕様を十分に理解すること

モックを使ってテストを行う際には、AWS SDK の仕様や制約を正しく理解していないと、誤ったテストを書くことになります。モックは本番環境のAWSサービスをシミュレートするためのツールですが、AWSサービスの特定の制約や動作を知らずにテストを作成すると、実際の挙動と異なる結果が出る可能性があります。そのため、AWS SDK の仕様を十分に理解してテストを設計することが重要です。

例:DynamoDB のBatchWriteItem における25件の制限

DynamoDB のBatchWriteItemAPI には、1回のリクエストで最大25件のアイテムしか書き込めないという制限があります。(より詳細な制約は公式ドキュメント 参照)
もしこの制約を理解せず、モックを使ったテストで大量のデータを一度に書き込む処理をテストした場合、実際の環境でこの制限によりエラーが発生する可能性があります。

誤ったモックのテスト例

以下は、DynamoDB に対して大量のアイテムを一度に書き込むテストですが、25件の制限を無視しています。このテストはモックを使っているためエラーが発生せず成功しますが、実際のAWS環境ではする可能性があります。

@Test
void 大量のデータをバッチ書き込みできること() {
  /* 準備 */
 List<WriteRequest> items = generateItems(30); // 25件を超えるアイテム

  /* 実行 */
  // dynamoDbServiceにて分割して書き込む機能がない場合、実際の環境では25件以上のアイテムがあるとエラーになる
  dynamoDbService.batchWriteItems("TestTable", items);

  /* 検証 */
  verify(dynamoDbClient).batchWriteItem(any(BatchWriteItemRequest.class));
}

このテストは、モック環境では成功しますが、DynamoDB の制限を理解していないため、実際の環境で25件を超えるリクエストがエラーになることに気づくことができません。テストを行う際は、以下のようなテストが必要です。

@Test
void DynamoDBのバッチ書き込み制限を考慮したテスト() {
  /* 準備 */
  List<WriteRequest> items = generateItems(30); // 25件を超えるアイテム

  /* 実行 */
  dynamoDbService.batchWriteItems("TestTable", items);

  /* 検証 */
  // 25件ずつ2回に分けて呼び出されることを確認
  verify(dynamoDbClient, times(2)).batchWriteItem(any(BatchWriteItemRequest.class));
}

このテストでは、30件のアイテムを生成し、dynamoDbService.batchWriteItems メソッドを呼び出します。そして、DynamoDB クライアントのbatchWriteItem メソッドが2回呼び出されることを検証します。これにより、25件の制限を考慮して適切に分割されていることを確認できます。

6.2 実際のAWSサービスとの違いを意識する

モックを使ったテストでは、実際のAWSサービスと異なる部分が存在することに注意が必要です。特に、パフォーマンスやスループットの面ではモックでは検証できない場合が多いため、負荷テストやパフォーマンステストは本番環境に近い条件で行う必要があります。

例:DynamoDB の読み書きスループット

DynamoDB ではプロビジョニングされたスループット(読み込みや書き込みのキャパシティ)があり、一定の制限を超えるとリクエストがスロットリングされます。モックではこのスロットリングを再現できないため、大量のデータを扱うパフォーマンステストは実際の環境で行う必要があります。

7. まとめ

Mockito を使用してAWS SDKのクライアントをモック化することで、効率的かつ安全に単体テストを行うことができます。依存性の注入やMockito のアノテーションを適切に使用し、AWSサービスの特有の制約を理解することで、信頼性の高いテストを実現できます。

7.1 依存性の注入を使用してAWSクライアントをモック化する

コンストラクタインジェクションを使用し、AWSクライアントをモック可能にします。実際の環境用とテスト用の2つのコンストラクタを用意します。

public class S3Service {
  private final S3Client s3Client;

  // モック用コンストラクタ
  public S3Service(S3Client s3Client) {
    this.s3Client = s3Client;
  }

  // 実環境用コンストラクタ
  public S3Service(Region region) {
    this(S3Client.builder().region(region).build());
  }
}

7.2 @Mockと@InjectMocksを適切に使用する

@Mock でモックオブジェクトを作成し、@InjectMocks でモックオブジェクトを注入します。

@ExtendWith(MockitoExtension.class)
public class S3ServiceTest {
  @Mock
  private S3Client s3Client;

  @InjectMocks
  private S3Service s3Service;
}

7.3 whenとverifyでメソッドの挙動と呼び出しを検証する

when でモックオブジェクトの振る舞いを定義し、verify でメソッドが期待通りに呼び出されたか確認します。

@Test
void ファイルをダウンロードできること() {
  /* 準備 */
  when(s3Client.getObject(any(GetObjectRequest.class), any()))
      .thenReturn(mockResponse);

  /* 実行 */
  s3Service.downloadFile("bucket", "key");

  /* 検証 */
  verify(s3Client).getObject(eq(expectedRequest), any());
}

7.4 例外が発生するケースをテストする

doThrow を使用して例外をスローするように設定し、assertThrows で例外が発生することを確認します。

@Test
void アップロード時に例外が発生した場合のテスト() {
  /* 準備 */
  doThrow(SdkClientException.class)
      .when(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));

  /* 実行と検証 */
  assertThrows(SdkClientException.class, () -> {
    s3Service.uploadData("bucket", "key", "data".getBytes());
  });
}

7.5 AWSサービスの制約や仕様を正しく理解する

AWS SDK のドキュメントを熟読し、制限や仕様を把握します。実際の環境での動作と一致するようにモックの振る舞いを設定します。

@Test
void DynamoDBのバッチ書き込み制限を考慮したテスト() {
  /* 準備 */
  List<WriteRequest> items = generateItems(30); // 25件を超えるアイテム

  /* 実行 */
  dynamoDbService.batchWriteItems("TestTable", items);

  /* 検証 */
  // 25件ずつ2回に分けて呼び出されることを確認
  verify(dynamoDbClient, times(2)).batchWriteItem(any(BatchWriteItemRequest.class));
}

8. 用語集

用語 説明
AWS SDK AWSサービスをプログラムから操作するためのライブラリ
モック 本来のサービスやオブジェクトの動作を模倣したテスト用のダミーオブジェクト
Mockito Javaのモックフレームワークで、テスト時に依存オブジェクトを模擬化するために使用するツール
Amazon DynamoDB AWSが提供するNoSQLデータベースサービス
Amazon S3 AWSが提供するオブジェクトストレージサービス
コンストラクタ オブジェクトの初期化時に呼び出されるメソッド
アノテーション コードに付加情報を与える記述で、特定の動作を指定するために使用する
テストフレームワーク テストを効率的に行うためのツールやライブラリの集合
エラーハンドリング エラーや例外が発生したときの処理方法を定義すること

9. Appendix

DynamoDbService、S3Service が実際のリソースに対して操作できるか確認するためのテストコードと、テストに必要なリソースを構築するためのCloudFormation テンプレート、build.gradle を記載します。

9.1 実リソースを操作するコード例

import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class Main {
  public static void main(String[] args) {
    // 使用するAWSリージョンを指定
    Region region = Region.AP_NORTHEAST_1; // 東京リージョンなど、お好みのリージョンに変更してください

    // DynamoDbServiceのインスタンスを作成
    DynamoDbService dynamoDbService = new DynamoDbService(region);

    // 操作するテーブル名を指定
    String tableName = "TestTable"; // 事前に作成しておく必要があります

    // DynamoDBに保存するアイテムを作成
    Map<String, AttributeValue> item = new HashMap<>();
    item.put("id", AttributeValue.builder().s("123").build());
    item.put("name", AttributeValue.builder().s("テストユーザー").build());
    item.put("email", AttributeValue.builder().s("test@example.com").build());

    // アイテムを保存
    log.info("DynamoDBにアイテムを保存します...");
    dynamoDbService.putItem(tableName, item);
    log.info("アイテムを保存しました。");

    // アイテムを取得するためのキーを作成
    Map<String, AttributeValue> key = new HashMap<>();
    key.put("id", AttributeValue.builder().s("123").build());

    // アイテムを取得
    log.info("DynamoDBからアイテムを取得します...");
    Map<String, AttributeValue> retrievedItem = dynamoDbService.getItem(tableName, key);
    log.info("取得したアイテム: " + retrievedItem);

    // S3Serviceのインスタンスを作成
    S3Service s3Service = new S3Service(region);

    // 操作するバケット名とオブジェクトキーを指定
    String bucketName = "test-tsuji-junit-sample-sdk"; // 事前に作成しておく必要があります
    String objectKey = "test.txt";

    // アップロードするデータを作成
    String content = "これはテストファイルの内容です。";
    byte[] data = content.getBytes(StandardCharsets.UTF_8);

    // ファイルをアップロード
    log.info("S3にファイルをアップロードします...");
    s3Service.uploadData(bucketName, objectKey, data);
    log.info("ファイルをアップロードしました。");

    // ファイルをダウンロード
    log.info("S3からファイルをダウンロードします...");
    byte[] downloadedData = s3Service.downloadFile(bucketName, objectKey);
    String downloadedContent = new String(downloadedData, StandardCharsets.UTF_8);
    log.info("ダウンロードしたファイルの内容: " + downloadedContent);
  }
}

ログにはlogback を利用しており、以下のような設定としています

<configuration>
    <property name="ROOT_LEVEL" value="INFO" />
    <appender name="jsonConsoleAppender"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder
                class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>Asia/Tokyo</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "Level": "%level",
                        "message": "%message%ex"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="jsonConsoleAppender" />
    </root>
</configuration>

9.3 CloudFormation テンプレート

以下のテンプレートでは、DynamoDB Table、S3 バケットを作成します。

AWSTemplateFormatVersion: '2010-09-09'
Description: >
  Template to create a DynamoDB table and an S3 bucket for testing DynamoDbService and S3Service.

Parameters:
  # Parameter to specify the S3 bucket name
  BucketName:
    Type: String
    Description: >
      The name of the S3 bucket to create.
      - Only lowercase letters, numbers, dots (.), and hyphens (-) are allowed.
      - Must be between 3 and 63 characters in length.
      - The bucket name must be globally unique.
    ConstraintDescription: S3 bucket name must be a valid format.

  # Parameter to specify the DynamoDB table name
  DynamoDBTableName:
    Type: String
    Description: >
      The name of the DynamoDB table to create.
      - Only alphanumeric characters (both uppercase and lowercase), underscores (_), hyphens (-), and dots (.) are allowed.
      - Must be between 3 and 255 characters in length.
      - Default value is "TestTable".
    Default: TestTable
    ConstraintDescription: DynamoDB table name must be a valid format.

Resources:
  # Creation of the DynamoDB table
  TestDynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Ref DynamoDBTableName
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST  # On-Demand Capacity Mode

  # Creation of the S3 bucket
  TestS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      AccessControl: Private
      # Additional properties like BucketPolicy can be added here if needed

Outputs:
  # Output the DynamoDB table name
  DynamoDBTableNameOutput:
    Description: The name of the created DynamoDB table
    Value: !Ref TestDynamoDBTable

  # Output the S3 bucket name
  S3BucketNameOutput:
    Description: The name of the created S3 bucket
    Value: !Ref TestS3Bucket

9.3 build.gradle

実リソース操作時用のlogback の設定等も含みます。

plugins {
  id 'java'
}

version = '1.0-SNAPSHOT'

java {
  sourceCompatibility = '17'
}

repositories {
  mavenCentral()
}

compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'

dependencies {
  // AWS SDK for DynamoDB
  implementation 'software.amazon.awssdk:dynamodb:2.28.1'
  // AWS SDK for S3
  implementation 'software.amazon.awssdk:s3:2.28.1'

  // JUnit 5
  testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'

  // Mockito
  testImplementation 'org.mockito:mockito-core:5.13.0'
  testImplementation 'org.mockito:mockito-junit-jupiter:5.13.0'

  // logger
  implementation 'ch.qos.logback:logback-classic:1.5.8'
  implementation 'net.logstash.logback:logstash-logback-encoder:8.0'
  compileOnly 'org.projectlombok:lombok:1.18.34'
  annotationProcessor 'org.projectlombok:lombok:1.18.34'
}

test {
  useJUnitPlatform()
}

tasks.withType(JavaCompile).configureEach {
  options.encoding = 'UTF-8'
}