[Java] テスタブルにリファクタして、プライベートメソッドをテストしていこう


この記事はソースコードがテスタブルになるようにリファクタを行い、プライベートメソッドをJavaのリフレクションを使ってテストしていく方法を紹介します。
※SpringFrameworkを利用している前提になります。

## リファクタ前
まず元のソースコードです。beforeパッケージに格納しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81962/7d994591-7eab-a6be-43b2-6f4d48410aaf.png)


こちらのUserServieクラスは、publicメソッド`calculateTimeZoneAge`つ持っています。フィールドに@AutowiredでUserRepositoryを注入しています。
UserRepositoryはDBアクセスするクラスでuserIdをもとに、DBにアクセスしてUserクラスを返却するメソッド`getUser`を持っています。
`calculateTimeZoneAge`というメソッドは、ユーザーが住んでいるタイムゾーンを考慮して、年齢を計算して返却するメソッドとなっています。

```java: UserService.java
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    /**
     * Userの住んでいるゾーンでの年齢を計算します。
     */
    public long calculateTimeZoneAge(int userId){
        User user = userRepository.getUser(userId);
        if (ObjectUtils.isEmpty(user)){
            throw new RuntimeException(String.format("UserId does not exist, userId={}", userId));
        }
        LocalDate birthday = LocalDate.parse(user.getBirthday(), DTF);// yyyy-MM-ddという文字列の誕生日からLocalDateを生成
        ZonedDateTime now = ZonedDateTime.now(user.getZoneId());// UserのZoneIdでの現在日時を取得
        LocalDate todayOnUserZone = LocalDate.of(now.getYear(), now.getMonth(), now.getDayOfMonth());// Userのnowから日付だけ取り出して、LocalDateを生成
        long age = ChronoUnit.YEARS.between(birthday, todayOnUserZone);// LocalDate同士の年の差を計算
        return age;
    }
}
```
```java: User.java
public class User {

    private int userId;
    /**
     * yyyy-mm-dd
     */
    private String birthday;
    /*
     * ユーザーが住んでいるゾーン
     * EX) US/Pacific (UTC-07:00), Asia/Tokyo (UTC+09:00), Asia/Manila (UTC+08:00)
     */
    private String zoneId;

    public User(int userId, String birthday, String zoneId) {
        this.userId = userId;
        this.birthday = birthday;
        this.zoneId = zoneId;
    }
...
```

### テストを試みる
さてこの`calculateTimeZoneAge`メソッドはテスタブルでしょうか？ためしに頑張って以下のようにテストを書いてみましょう。
```java: UserServiceTest.java
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void calculateAge(){
        long age = userService.calculateTimeZoneAge(???);// userIdどうしたらいい？
    }
}
```
まずここで止まると思います。
`calculateTimeZoneAge`メソッドに渡すuserIdをどうしたらいいでしょうか？
そもそもUnitTestの中でDBにアクセスしないとUserのデータが取れないしと悩むと思います。
他にも以下のような問題に当たると思います。
* Repositoryクラスに強く依存しているので、そちらの挙動に左右される。（=DBアクセスがある）
* LocalDate.parseの挙動のみをテストしたいのにできない。
* ChronoUnit.YEARS.betweenの挙動のみをテストしたいのにできない。
* userIdが存在しない場合の挙動をチェックしたいのにできない。

う〜ん、なかなかテストが書きづらいですね。では、リファクタしてみましょう。

## リファクタしてみよう
まずリファクタ後のパッケージ構造です。
UserRepositoryとUserServiceにはインターフェースを定義して、インターフェースに対して実装するようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81962/4290f92a-cf0c-56df-0420-8c8a3a5be466.png)

そしてUserServiceクラスはUserServiceImplクラスとなり、以下のように生まれ変わりました。
```java: UserServiceImpl.java
@Service
public class UserServiceImpl implements UserServiceInterface{

    private UserRepositoryInterface userRepository;

    private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    @Autowired
    public UserServiceImpl(UserRepositoryInterface userRepository){
        this.userRepository = userRepository;
    }

    // Userの住んでいるゾーンでの年齢を計算します。
    public long calculateTimeZoneAge(int userId){
        User user = userRepository.getUser(userId);
        if (ObjectUtils.isEmpty(user)){
            throw new RuntimeException(String.format("UserId does not exist, userId=%s", userId));
        }
        LocalDate birthday = convertLocalDate(user.getBirthday());
        LocalDate todayOnUserZone = generateNowLocalDateFromTimeZone(user.getZoneId());
        long age = this.calculateYearsBetween(birthday, todayOnUserZone);
        return age;
    }
    // yyyy-MM-ddという文字列から、LocalDateを生成
    private LocalDate convertLocalDate(String dateStr){
        return LocalDate.parse(dateStr, DTF);
    }
    // zoneIdから現時点でのLocalDateを生成
    private LocalDate generateNowLocalDateFromTimeZone(String zoneId){
        ZonedDateTime now = ZonedDateTime.now(ZoneId.of(zoneId));
        LocalDate todayFromZoneId = LocalDate.of(now.getYear(), now.getMonth(), now.getDayOfMonth());
        return todayFromZoneId;
    }
    // LocalDateを比べて、何年差あるのか計算する
    private long calculateYearsBetween(LocalDate from, LocalDate to){
        return ChronoUnit.YEARS.between(from, to);
    }
}
```

大きな変更としては２つあります。
- リファクタ前はUserRepositoryを直接フィールドにAutowiredで依存性を注入していましたが、依存性を下げるためにUserRepositoryImplへの直接の参照ではなく、**UserRepositoryInterfaceとインターフェースへの参照に変更しました。また、フィールドでの依存注入ではなくて、コンストラクターから依存性を注入する** ように変更しました。
- `calculateTimeZoneAge`のメソッドの中身を**プライベートメソッドに切り出し**ていきました。今回はサンプルなので過剰に切り出しています。

この変更により、ユニットテストクラスはどのように変化したでしょうか？

```java: UserServiceImplTest.java
@SpringBootTest
public class UserServiceImplTest {

    @Test
    public void calculateTimeZoneAge(){
        UserRepositoryInterface mockUserRepo = mock(UserRepositoryInterface.class);// Mockを生成
        when(mockUserRepo.getUser(1)).thenReturn(new User(1, "2001-02-04", "America/Los_Angeles"));// Stubメソッド設定
        UserServiceImpl userService = new UserServiceImpl(mockUserRepo);// MockのRepositoryクラスをコンストラクタから注入して、UserServiceImplを生成
        long age = userService.calculateTimeZoneAge(1);
        assertTrue(age > 20);// 現在時刻に依存して、チェックが難しいので、実装日より将来かどうかだけをチェックする
    }

    // Exceptionのテスト
    @Test
    public void calculateTimeZoneAgeUserNotExist(){
        UserServiceImpl userService = new UserServiceImpl(mock(UserRepositoryInterface.class));
        try {
            userService.calculateTimeZoneAge(2);// UserId=2は存在しないので、エラーになる
        } catch (RuntimeException e) {
            assertTrue(e.getMessage().startsWith("UserId does not exist"));
            return;
        }
        assertTrue(false, "Should not reach here");
    }
...
}

```

非常にテストが書きやすくなりました。
まずMockitoを利用して、UserRepositoryImplへの依存をコンストラクターで注入することができています。**これこそがAutowiredや依存性注入機能のメリット**です。フィールドに対して依存性を注入しているとこのようなことまできません（Reflection使えばできなくはないですが、やりすぎです）。

また、クラス間の依存を減らしたり、別の実装を差し込めるようにするためにもInterfaceに依存してもらうようにしています。Mockのお作法的にもInterfaceをMockする感じではあると思うのですが、まぁ過剰にインターフェース作りすぎなくてもいいと思います。実装クラスもMockにできますので。

### プライベートメソッドをテストする
そしてプライベートメソッドである以下をReflectionを使って強引にテストします。
例えば、以下のプライベートメソッド`calculateYearsBetween`をテストしてみましょう。
```java
    private long calculateYearsBetween(LocalDate from, LocalDate to){
        return ChronoUnit.YEARS.between(from, to);
    }
```

```java
    @Test
    public void calculateYearsBetween() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{
        UserServiceImpl userService = new UserServiceImpl(null);//mockすら不要
        Method method = userService.getClass().getDeclaredMethod("calculateYearsBetween", LocalDate.class, LocalDate.class);// ReflectionでMethodを定義
        method.setAccessible(true);// プライベートメソッドアクセス許可する
        long result = (long) method.invoke(userService, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 12,31));// Methodを実行する
        assertEquals(result, 0);
        // 境界値テスト
        result = (long) method.invoke(userService, LocalDate.of(2010, 2, 1), LocalDate.of(2020, 1,31));
        assertEquals(result, 9);
        result = (long) method.invoke(userService, LocalDate.of(2010, 2, 1), LocalDate.of(2020, 2,1));
        assertEquals(result, 10);
        // うるう年テスト
        result = (long) method.invoke(userService, LocalDate.of(2008, 2, 29), LocalDate.of(2018, 2,28));
        assertEquals(result, 9);
        result = (long) method.invoke(userService, LocalDate.of(2008, 2, 29), LocalDate.of(2018, 3,1));
        assertEquals(result, 10);
        result = (long) method.invoke(userService, LocalDate.of(2008, 2, 29), LocalDate.of(2020, 2,29));
        assertEquals(result, 12);
    }
```

このようにリファクタすることで以下のようなメリットがあります。
- 外部クラスへの依存をConstructorを使って渡す方法を取ることにより、依存クラスのmock化がしやすくなります。
- privateメソッドを直接テストすることが可能です
- TDDな開発が非常にしやすいです
- メソッド内で外部クラスへの依存がなければ、mock化すら不要です

小さいメソッドに分割して、確実にテストしながら進めていくことで、最終的にはそれらの小さいメソッドをメインのメソッド（今回だと`calculateTimeZoneAge`）から呼べば実装完了となるようにしましょう。メインのメソッドにロジックを持たすとまたそれもテストしないといけなくなるので、ロジックはprivateメソッドに切り出して、メインのメソッドは「ただ呼び出すだけ」くらいの役割にしておくことをお勧めします。またコードの見通しも立てやすいです。

今回のコードは以下にあります。
https://github.com/nishiyama-k0130/private-method-unit-test

## 今回の学び
- 依存はコンストラクタから設定しましょう
- プライベートメソッドに処理を分割していって、細かくテストしましょう

以上