SpringBoot + JUnit + Mockito で UnitTest

SpringBootのプロジェクトにCIを導入したくて、自動テストについて調べていたところ、いろいろなツール名が出てきて混乱したので整理がてらメモ書きを。
自動テスト初心者の記事なので嘘ついてたらごめんなさい。わざとじゃないです。

最終的にできたプロジェクトは[こちら](https://github.com/RichterAstell/junit-sample)


# TL;DR

1. JUnit でテストクラスを実行する
2. Mockito でモックを作成して依存先を置き換える
3. private メソッドはリフレクションでテストができる
4. Hamcrest はアサーション用のマッチャー

# 目次
[1.用語](#1-用語)
[2.サンプルプロジェクトの作成](#2-サンプルプロジェクトの作成)
[3.JUnitで単体テストの作成](#3-JUnitで単体テストの作成)
[4.Mockitoでモックを使用したテストの作成](#4-Mockitoでモックを使用したテストの作成)
[5.privateメソッドのテスト](#5-privateメソッドのテスト)
[6.テスト結果レポート](#6-テスト結果レポート)
[7.おわりに](#7-おわりに)

#1. 用語

登場する用語を簡単に並べる

+ SpringBoot
    + Javaのフレームワーク
+ JUnit
    + Javaの単体テスト向けテストフレームワーク
    + 単体テストはこいつを使って実行されることが多い
+ Mokito
    + Javaの単体テスト向けモックフレームワーク
    + テスト対象が依存するモジュールをダミーとして置き換える
+ Report
    + テストの実行結果をみやすく表示したhtml郡
+ 依存関係
    + Class A の処理で Class B を使用している場合、Class A はClass B に依存しているといいます
+ カバレッジ
    + 自動テストでどの程度確認できているかの指標

#2. サンプルプロジェクトの作成

なにはともあれ、テストを行うためのプロジェクトを作成します。
なんでもいいので Spring initializer で適当に作成しました。

| key          | val                   |
| ------------ | --------------------- |
| Project      | Gradle project        |
| Language     | Java                  |
| Spring Boot  | 2.4.1                 |
| Group        | com.sample            |
| Artifact     | testsample            |
| Name         | testsample            |
| Description  | Sample project        |
| Package name | com.sample.testsample |
| Packaging    | Jar                   |
| Java         | 11                    |
| Dependencies | Spring Web            |
|              | H2 Database           |
|              | Spring Data JPA       |
|              | Lombok                | 

プロジェクトができたらよくあるユーザ登録風の処理を作成しましょう。
追加、変更したファイルは以下

+ testsample
    1. UserController.java
    2. UserEntity.java
    3. UserRepository.java
    4. UserService.java
+ resources
    1. application.properties
    2. schema.sql

src部分のディレクトリ構成はこんな感じ

```
src
├── main
│   ├── java
│   │   └── com
│   │       └── sample
│   │           └── testsample
│   │               ├── TestsampleApplication.java
│   │               ├── UserController.java
│   │               ├── UserEntity.java
│   │               ├── UserRepository.java
│   │               └── UserService.java
│   └── resources
│       ├── application.properties
│       ├── schema.sql
│       ├── static
│       └── templates
└── test
    └── java
        └── com
            └── sample
                └── testsample
                    └── TestsampleApplicationTests.java
```

ファイルの内容

```java:UserController.java
package com.sample.testsample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

@RestController
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping(value="/index", method = RequestMethod.GET)
    public String index() {
        return "This page is user page";
    }

    @RequestMapping(value="/add", method = RequestMethod.POST)
    public String add(@RequestParam String name,
                      @DateTimeFormat(pattern = "yyyy-MM-dd")
                      @RequestParam LocalDate birthday) {
        userService.addUser(name, birthday);
        return "Success!!";
    }
}
```

```java:UserEntity.java
package com.sample.testsample;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = "user")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private Long age;
}
```
※ Lombok の `@Data` が動かない場合、手動でアクセサを追加してもOK

```java:UserRepository.java
package com.sample.testsample;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}
```

```java:UserService.java
package com.sample.testsample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

@Service
public class UserService {
    @Autowired
    UserRepository userRepository;

    public void addUser(String name, LocalDate birthday) {
        // User entity の作成
        UserEntity entity = makeUserEntity(name, birthday);
        // 保存
        userRepository.save(entity);
    }

    private UserEntity makeUserEntity(String name, LocalDate birthday) {
        // User entity の作成
        UserEntity entity = new UserEntity();
        entity.setName(name);
        // 年齢の算出・設定
        LocalDate now = LocalDate.now();
        Long age = ChronoUnit.YEARS.between(birthday, now);
        entity.setAge(age);
        return entity;
    }
}
```

```properties:application.properties
spring.datasource.url=jdbc:h2:./test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=testsample
spring.datasource.password=testsample
spring.datasource.sql-script-encoding=UTF-8
spring.datasource.initialization-mode=always
spring.datasource.schema=classpath:schema.sql
```

```sql:schema.sql
DROP TABLE user;
CREATE TABLE user (
  id INTEGER NOT NULL AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL,
  age INTEGER NOT NULL,
  PRIMARY KEY (id)
);
```

さて、ここまでで登録処理ができたので次にテストを作っていきましょう

#3. JUnitで単体テストの作成

テストクラスは `test` パッケージ配下に xxxTests.java の命名規則で作成していきます。
今回の場合ロジックは `UserService.java` にあるので、これをテストしていきたいので `UserServiceTests.java` を追加します。
まずは簡単にテストできそうな `calckAge()` のテストから実装していきましょう。

```java:UserServiceTests.java
package com.sample.testsample;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;

public class UserServiceTests {
    @Test
    public void 年齢算出のテスト() {
        UserService service = new UserService(null);
        LocalDate date = LocalDate.of(2000, 01, 01);
        Long age = service.calcAge(date);
        Assertions.assertEquals(21l, age);
    }
}
```

2021/1/11 現在、このテストは成功しますが問題が一つありますね。
来年になるとテストが失敗してしまいます :scream:
解決策としていくつか考えてみます...

1. calcAge に LocalDate の引数を追加して差分を算出する
2. 日付を取得するサービスを経由するような構成にして Interface を活用して日付を取得する
3. 日付を取得するサービスを経由するような構成にしてモックを活用してテスト用に値を上書きする

などなど、いくつか考えられますが、 2 か 3 の手段が色々とやりやすいようですので、今回は 3 の手段を採用します。

#4. Mockitoでモックを使用したテストの作成

日付取得用のサービス `DateUtils.java` を追加して、 `UserService` と `UserServiceTests` を書き換えましょう

```java:DateUtils.java
package com.sample.testsample;

import org.springframework.stereotype.Component;

import java.time.LocalDate;

@Component
public class DateUtils {
    public LocalDate getNowDate() {
        return LocalDate.now();
    }
}
```

```diff:UserService.java
 package com.sample.testsample;

 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;

 import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;

 @Service
 public class UserService {
+    private final DateUtils dateUtils;
     private final UserRepository userRepository;

     @Autowired
     public UserService(UserRepository userRepository
+        , DateUtils dateUtils) {
         this.userRepository = userRepository;
+        this.dateUtils = dateUtils;
     }

...

 public Long calcAge(LocalDate birthday) {
-        LocalDate now = LocalDate.now();
-        Long age = ChronoUnit.YEARS.between(birthday, now);
+        Long age = ChronoUnit.YEARS.between(birthday, dateUtils.getNowDate());
         return age;
     }
 }
```

```diff:UserServiceTests.java
 package com.sample.testsample;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import java.time.LocalDate;
 
 public class UserServiceTests {
     @Test
     public void 年齢算出のテスト() {
+        // Mockの作成
+        DateUtils dateUtils = Mockito.mock(DateUtils.class);
+        Mockito.when(dateUtils.getNowDate()).thenReturn(LocalDate.of(2021, 1, 11));
 
-        UserService service = new UserService(null);
+        UserService service = new UserService(null, dateUtils);
         LocalDate date = LocalDate.of(2000, 01, 01);
         Long age = service.calcAge(date);
         Assertions.assertEquals(21l, age);
     }
 }
```

`Mockito.mock()` で `DateUtils` と同じメソッドを持ったモックを作成します。
ただし、モックですので呼び出したメソッドは null を返却してしまいます。
そこで `Mockito.when()` と `thenReturn()` を使って `dateUtils.getNowDate()` の戻り値を上書きしてやります。
そうすることでこのテストは現在日付によらず成功を確認できるわけですね。

ポイントとしては `UserService` クラスの依存関係をコンストラクタインジェクションで解決させること。そうすることでモックの注入がしやすくなって幸せです :blush:

さて、次に `UserEntity` 作成メソッドのテストを作っていきましょう。

#5. privateメソッドのテスト

`UserEntitiy` は `makeUserEntity()` で作成されるのですが、このメソッドのアクセス修飾子は private です。
そのままでは呼び出しができないので、リフレクションを使用してテストを作成していきましょう。

`UserServiceTests` にテストケースを追加しましょう。

```diff:UserServiceTests.java
 public class UserServiceTests {
 /* write other test cases */
+    @Test
+    public void ユーザエンティティ作成のテスト() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+        // Mockの作成
+        DateUtils dateUtils = Mockito.mock(DateUtils.class);
+        Mockito.when(dateUtils.getNowDate()).thenReturn(LocalDate.of(2021, 1, 11));
+        // reflection で private メソッドを取得
+        UserService service = new UserService(null, dateUtils);
+        Method method = service.getClass().getDeclaredMethod("makeUserEntity", String.class, LocalDate.class);
+        method.setAccessible(true);
+        UserEntity entity = (UserEntity) method.invoke(service, "Richter", LocalDate.of(2000, 1, 1));
+        // 結果の比較
+        Assertions.assertEquals(null, entity.getId());
+        Assertions.assertEquals("Richter", entity.getName());
+        Assertions.assertEquals(21l, entity.getAge());
+    }
 }
```

private メソッドのテスト方法はこちらを参考にさせていただきました。大感謝！ :kissing_closed_eyes:
[Junitでprivateメソッドのテスト方法 - Qiita](https://qiita.com/village/items/2f0d99b25eef0c8fd4ec)

#6. テスト結果レポート

さて、現在はサンプルなのでこの程度しかテストケースがありませんが、実際のシステムはもっとたくさんのテストケースが実行されていることも多いでしょう。
テストの結果をみやすく表示するためにテスト結果のレポートを確認してみましょう.
テストを実行すると
`<project_root>/build/reports/tests/test/index.html`
にテスト結果のレポートが作成されていますので確認してみましょう。

#7. おわりに

以上でとても簡単ですが SpringBoot アプリケーションに JUnit でのテストを組み込んでみました。
誰かのはじめの一歩のサポートになれば幸いです。