ソフトウェアエンジニアの雑記

日々思ったことをまとめます

SpringBootでLocalDateTime.now()などの時刻管理を使用したときに、ユニットテストできるようにする方法

はじめに

SpringBootを利用していると、静的メソッド(LocalDateTime.now()など)を使用したくなる場面があります。静的メソッドはユニットテストをしづらくする側面があるのでなるべく使用を控えたいので、その回避方法になります。

目次

要約

要約としては、下記の2点になります。

  • java.util.Clockを利用する
  • @ConfigurationでBean化する
  • 利用したいクラスでDIする

静的メソッドのメリット・デメリット

静的メソッドは便利ですが、状態を持つと途端に面倒なツールに早変わりします。

デメリットとしては、

  • DIとの相性が悪い
  • テスタビリティが低い(PowerMockやPowerMockito、Mockito.mockStaticなどを使用する必要があり、十分ではない)
  • グローバルな情報を参照していると予期しない副作用のリスク
  • マルチスレッド処理での懸念

などなど、小さいアプリケーションではあんまりデメリットにはならないですが、ユニットテストが威力を発揮するような中〜大規模になってくると状態をもった静的メソッドは邪魔にしかなりません。よくあるのが、LocalDateTime.now()などをコアロジックで直接書いてしまうと、ユニットテスト時にMockito.mockStaticやPowerMockitoなどを使用しないといけなく不便になります。

特に、テスト対象のメソッド内で複数スレッドを作成し、その中でLocalDateTimeを呼んでいる場合は、Mockito.mockStaticで設定しても、範囲外なので設計を大きく直す必要があります。

Clock を使った解決策

1. Clock のBean定義

まず、ClockをSpringのBeanとして定義します。

@Configuration
public class ClockConfig {
    
    @Bean
    public Clock clock() {
        return Clock.systemDefaultZone();
    }
}

2. Service層での利用

ClockをDIして使用します。

@Service
@RequiredArgsConstructor
public class ClockServiceImpl implements ClockService {
    private final Clock clock;

    @Override
    public LocalDateTime getAfter5Minute() {
        return LocalDateTime.now(clock).plusMinutes(5);
    }
}

3. テストでの活用

テストでは、固定された時刻を返すClockを使用できます。

@ExtendWith(MockitoExtension.class)
class ClockServiceImplTest {

    @InjectMocks
    private ClockServiceImpl clockService;

    @Spy
    private Clock clock = Clock.fixed(Instant.parse("2026-01-01T00:00:00z"), Clock.systemUTC().getZone());;

    @Test
    void getNow() {
        LocalDateTime now = clockService.getAfter5Minute();
        assertEquals(LocalDateTime.parse("2026-01-01T00:05:00"), now);
    }
}

上記のようにすれば、ミリ秒やマイクロ秒の差でのテスト失敗が発生しません。

まとめ

Spring BootのDIの思想に沿ってClockを活用することで下記の恩恵に預かれます。

  • テストが容易になる
  • 時刻を完全に制御できる
  • タイムゾーンを明示的に管理できる
  • コードの保守性が向上する

小さな変更ですが、テスタビリティと保守性に大きな違いをもたらします。ぜひ実践してみてください。