SpringBootでsmall test　JUnit5 × Mockito　ver

## はじめに
SpringBootでテストを、となった際にテストサイズを考えsmall testのUnitテストを実装したいという場面があるが、small testをどのように実装するのかについて記載した記事をあまり見かけないので、自身の備忘録としても残しておく。
**※ググると大抵dependencyに`spring test`系を持たせているものが見つかるが、ミニマムのunit testを実装する上ではそれらは不要。**

この記事を読むと分かる事は以下。

- SpringBootにおけるMockitoを用いたsmall testを導入する際の依存関係（`pom.xml`の内容）
- DI(`@Autowired`)されているフィールドの扱い方などテストコードの基本的な書き方
- 細かいテストの記述方法やテクニック
  - [static methodのmock化の方法](#static-methodのmock化の方法)
  - [テスト対象のメソッドで、自身のクラスのメソッド(private以外)を呼び出すような場合にテストを簡単にするテクニック（`spy`の利用）](#テスト対象のメソッドで自身のクラスのメソッドprivate以外を呼び出すような場合にテストを簡単にするテクニックspyの利用)
  - [共通のmockの定義方法](#共通のmockの定義方法引数が可変長のもののmock化方法)
  - [引数が可変長のもののmock化方法](#共通のmockの定義方法引数が可変長のもののmock化方法)
  - [MockMultipartFileを使ったmock化の方法](#mockmultipartfileを使ったmock化の方法)

## small testとは
そもそもsmall testとはどういうもの？という話になるが、small testとは以下のようなテストとして定義できる。

- ネットワークへアクセスしない
- データベースへアクセスしない
- ファイルシステムへアクセスしない
- 外部システムへアクセスしない
- シングルスレッドで動作する
- システムプロパティへのアクセスをしない

## カバレッジの話
上記のsmall testの概念に加えて、ソースコードのテストにおいて切り離せない話でカバレッジがあるが、
いずれのカバレッジでもsmall testは実現できるので、ここではどのカバレッジで実装するかは扱わない。

## JUnit5×Mockitoでsmall test
### SpringBootにおけるMockitoでのsmall testを導入する際の依存関係（`pom.xml`の内容）
プロジェクト(パッケージ)管理ツールにmavenを使っている場合、pom.xmlは以下のようになる。
※色々省略しているが、samll testであれば基本的に`spring-boot-starter-test`などの依存は不要。

```xml:pom.xml
<project xmlns="・・・">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>

    <properties>
        <junit-jupiter.version>5.7.1</junit-jupiter.version>
        <mockito.version>3.7.7</mockito.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- jdbc, json, lombokなどなど -->
        <dependency>
            <groupId>com.googlecode.json-simple</groupId>
            <artifactId>json-simple</artifactId>
            <version>1.1.1</version>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
```
ここでは

- static methodのMock化が行えるように`mockito-inline`を依存に追加
- JUnitの依存を持つ`json-simple`のJUnit依存をexclusionに指定し除外<br>※自動的に依存が追加されるような場合は`exclusion`を使い依存を明示的に除外する

をしている。
ここで注意として、MockitoであればPowerMockを使いたい場面もあるかもしれないが、現時点でJunit5では[使えないよう・・・](https://github.com/powermock/powermock/issues/929)。

### DI(`@Autowired`)されているフィールドの扱い方などテストコードの基本的な書き方
まずMockitoを使うため、`@ExtendWith`で`MockitoExtension.class`を宣言し継承する。
SpringでDI(`@Autowired`)されている各フィールドは`@Mock`でmock化する。
また、それらをtest対象のクラスにinjectするために、test対象クラスに対しては`@InjectMocks`を記載しmockを注入する。
基本的なパターンとしては以下のような実装になる。
※Mockitoの使い方・構文については[ここ](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html)を参照。

```java:SampleTest.java
// 省略
@ExtendWith(MockitoExtension.class)
class SampleTest {
  
  @Mock
  private Hoge mockHoge;
  @Mock
  private Fuga mockFuga;

  @InjectMocks
  private Sample testClass;

  @Test
  void testFoo() {
    doReturn("hoge").when(mockHoge).getHoge();
    assertEquals("hoge", testClass.foo());
  }
  // 省略
}
```

### 細かいテストの記述方法やテクニック
#### static methodのmock化の方法
`MockedStatic`を利用する。

```java:MockStaticSampleTest.java
// 省略
@ExtendWith(MockitoExtension.class)
class MockStaticSampleTest {
  
  @Mock
  private Hoge mockHoge;

  @InjectMocks
  private MockStaticSample testClass;

  @Test
  void testGetCompleteDate() {
    try (MockedStatic<DateUtil> mocked = mockStatic(DateUtil.class)) {
      mocked.when(() -> DateUtil.getNumbersOnlyFormat(anyString()).thenReturn("19700101");                             
      assertEquals("19700101", testClass.getCompleteDate());
      mocked.verify(() -> DateUtil.getNumbersOnlyFormat(anyString());
    }  
  }
  // 省略
}
```

#### テスト対象のメソッドで、自身のクラスのメソッド(private以外)を呼び出すような場合にテストを簡単にするテクニック（`spy`の利用）
以下のようにテスト対象のメソッド内で、自身の他のメソッド(private以外)が呼ばれるような場合、`spy`を使うと一部だけmock化ができテストを書きやすくなる。
具体的には、自身のメソッドを呼び出しその戻り値を使って分岐をするようなメソッドをテストする際に、カバレッジをC2など複雑なものを採用すると、自身のメソッドの返り値まで考えて複雑な実装しなければならず大変になるが、それを簡単にする事ができる。

```java:PrivateContainSample.java
// 省略
class PrivateContainSample {
  
  SampleContentsDto foo() {
    SampleDto dto = generate(commonDto);
    // 何かの処理
  }

  SampleDto generate(SampleCommonDto dot) {
    // 何かの処理
  } 
  // 省略
}
```

```java:PrivateContainSampleTest.java
// 省略
@ExtendWith(MockitoExtension.class)
class MockStaticSampleTest {
  
  @Mock
  private Hoge mockHoge;

  @InjectMocks
  private PrivateContainSample testClass;
  private PrivateContainSample sptTestClass;

  @BeforeEach
  void setUp() {
    spyTest = spy(testClass);
  }

  @Test
  void testFoo() {
    doReturn(new SampleDto()).when(sptTestClass).generate(any(SampleCommonDto.class));
    assertNotNull(spyTestClass.foo());
  }
  // 省略
}
```

`spy`で記述した部分（`generate`）だけがmock化されるので、`foo`はmock化されず通常通りテストが実施できる。

#### 共通のmockの定義方法・引数が可変長のもののmock化方法
テスト対象のクラス全体で使われているメソッドで、1つの共通の戻り値があればいいような場合は、`@BeforeEach`でmock化してしまうと便利。
また、可変長引数の場合は`Mockito.<String[]>any()`のように記述する。

```java:SampleTest.java
// 省略
@ExtendWith(MockitoExtension.class)
class SampleTest {
  
  @Mock
  private Hoge mockHoge;
  @Mock
  private CommonMessage mockCommonMessage;

  @InjectMocks
  private Sample testClass;

  @BeforeEach
  void setUp() {
    doReturn(new Message()).when(mockCommonMessage).getMessage(Mockito.<String[]>any());
  }

  @Test
  void testFoo() {
    // 何かのテスト
  }
  // 省略
}
```

#### MockMultipartFileを使ったmock化の方法
```java:MockMultipartFile.java
/**
 * Create MockMultipartFile used for JUnit test
 *
 * @param fileName File name used for testing
 * @return MockMultipartFile
 */
private MockMultipartFile createTestFile(String fileName) throws Exception {
  MockMultipartFile multipartFile;
  
  String path = new File(".").getAbsoluteFile().getParent();
  File file = new File(path + "/src/test/resources/hogehoge/" + fileName + ".xlsx");

  try (FileInputStream input = new FileInputStream(file)) {
    multipartFile = new MockMultipartFile("file", file.getName(), CONTENT_TYPE, IOUtils.toByteArray(input));
  }
  return multipartFile;
}
```

## 参考文献
- https://testing.googleblog.com/2010/12/test-sizes.html