S3などのデータストアへのアクセス機能をモックとすることでデータストアなしでユニットテストする手法( Java, Mockito )

# 概要

データストアにアクセスすることなくテストを可能とするポイントを紹介します。

1. データストア( 今回はS3 )にアクセスするオブジェクトを、外部からテスト対象クラスに注入可能としておく
1. テスト時は、データストアにアクセスしないモックを注入する

動作確認可能なソースコードは [loftkun/spring-boot-s3-mock-example](https://github.com/loftkun/spring-boot-s3-mock-example) にあります。

## テスト対象クラス

テスト対象のクラスです。S3にアクセスする`client`を外部から注入できるようにしています。
( この例ではコンストラクタで受け取っています。 )

このようにデータストアにアクセスするオブジェクトを、外部からテスト対象クラスに注入できるように実装しておきます。これがいわゆる依存性注入 ( DI : Dependency Injection ) です。

のちほど紹介するテストでは、データストアにアクセスしないモックを注入します。
これにより、テスト用データストアの準備が不要となります。

```java
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.Bucket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestTargetClass {

    private static final Logger logger = LoggerFactory.getLogger(TestTargetClass.class);

    private AmazonS3 client = null;

    public TestTargetClass(AmazonS3 client) {
        this.client = client;
	}

	public boolean testTargetMethod(String bucketName){
        if (client.doesBucketExistV2(bucketName)) {
            logger.info("doesBucketExistV2");
            return false;
        }
        Bucket bucket = client.createBucket(bucketName);
        logger.info(bucket.toString());
        logger.info(client.getBucketLocation(bucketName));
        return true;
    }
}
```

テスト対象のメソッドは`testTargetMethod（）`です。S3にバケットを作成する単純なメソッドです。
このメソッドのテストケースは以下2つです。

1. バケットが存在しなければ作成し、作成したバケットの情報をログに出力する。
1. バケットが既に存在する場合はその旨をログに出力し、バケットを作成しない

続いて、これらのテストケースをモックを用いて検証する手法を紹介します。

## テストクラス

テストメソッドは以下2つを実装しています。

テストケース | メソッド |
--- | --- |
1 | create_bucket()
2 | not_create_bucket()

モックフレームワークは [Mockito](https://site.mockito.org/) を利用しました。
以下のようにして`client`のモックを作成しています。

```java
AmazonS3 client = mock(AmazonS3.class);
```

作成したモックをテスト対象のオブジェクトに注入します。
今回はコンストラクタで注入していますが、setterでも構いません。
フレームワークが提供するDI機能( DIコンテナ )を活用するのも良いです。

```java
TestTargetClass testTarget = new TestTargetClass(client);
```

これにより、テストのためにデータストアを準備する必要がなくなります。

```java
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.Bucket;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestTargetClassTest {

    private static final Logger logger = LoggerFactory.getLogger(TestTargetClassTest.class);

    @Test
    void create_bucket() {

        String bucketName = "testBucketName";
        String bucketLocation = "testBucketLocation";

        // define mock
        Bucket bucket = mock(Bucket.class);
        when(bucket.getName()).thenReturn(bucketName);
        AmazonS3 client = mock(AmazonS3.class);
        when(client.doesBucketExistV2(bucketName)).thenReturn(false);
        when(client.createBucket(bucketName)).thenReturn(bucket);
        when(client.getBucketLocation(bucketName)).thenReturn(bucketLocation);

        // test target
        TestTargetClass testTarget = new TestTargetClass(client);
        boolean result = testTarget.testTargetMethod(bucketName);

        // verify
        verify(client, times(1)).doesBucketExistV2(bucketName);
        verify(client, times(1)).createBucket(bucketName);
        verify(client, times(1)).getBucketLocation(bucketName);
        assertEquals(true, result);
    }

    @Test
    void not_create_bucket() {

        String bucketName = "testBucketName";

        // define mock
        Bucket bucket = mock(Bucket.class);
        when(bucket.getName()).thenReturn(bucketName);
        AmazonS3 client = mock(AmazonS3.class);
        when(client.doesBucketExistV2(bucketName)).thenReturn(true);

        // test target
        TestTargetClass testTarget = new TestTargetClass(client);
        boolean result = testTarget.testTargetMethod(bucketName);

        // verify
        verify(client, times(1)).doesBucketExistV2(bucketName);
        verify(client, times(0)).createBucket(bucketName);
        verify(client, times(0)).getBucketLocation(bucketName);
        assertEquals(false, result);
    }
}
```

## テストケース1

### モックの定義

以下のようにして`doesBucketExistV2()`が呼び出された場合、`false`を返すモックとしています。
これにより同名の既存バケットが存在しないという条件を与えることにより、テストケース1をテストできるようにしています。

```java
when(client.doesBucketExistV2(bucketName)).thenReturn(false);
```

### 検証

以下を検証しています。

- 以下メソッドが1回呼び出されること
    - doesBucketExistV2()
        - 既存バケットの存在チェックを行なっていること
    - createBucket()
        - バケットを作成すること
    - getBucketLocation()
        - 作成したバケットの情報を取得していること
- 戻り値がtrueであること
    - 今回は例ということで期待値をtrueとしています

## テストケース2

### モックの定義

以下のようにして`doesBucketExistV2()`が呼び出された場合、`true`を返すモックとしています。
これにより同名の既存バケットが存在するという条件を与えることにより、テストケース2をテストできるようにしています。

```java
when(client.doesBucketExistV2(bucketName)).thenReturn(true);
```

### 検証

以下を検証しています。

- 以下メソッドが1回呼び出されること
    - doesBucketExistV2()
        - 既存バケットの存在チェックを行なっていること
- 以下メソッドは呼び出されないこと
    - createBucket()
        - バケットを作成しないこと
    - getBucketLocation()
        - バケットの情報を取得していないこと
- 戻り値がfalseであること
    - 今回は例ということで期待値をfalseとしています

## まとめ

データストアに実際にアクセスすることなくテストを可能とするポイントを紹介しました。

- データストアにアクセスするオブジェクトをDI可能としておく
- テスト時にはモックをDIする

データストアに依存する条件をモックとして与えることにより、テスト用のデータストアの準備が不要となります。
本記事がテスト手法の参考になりましたら幸いです。