Thread.sleep を wrap してテストを書きやすくする
Thread.sleep
を使うコードのテストは書きづらい。呼び出し前の時刻と呼び出し後の時刻を比較してなんとかテストしようとするのだが、条件を厳しくすると誤差が出て失敗することもあるし、sleep に実時間が掛かってテストが遅くなってしまう。
そこで、以下のような Sleeper
クラスを導入することにした。
class Sleeper {
fun sleep(duration: Duration) {
require(!duration.isNegative)
Thread.sleep(duration.toMillis())
}
}
見ての通り単に Thread.sleep
を呼び出すだけだが、こうしておくことで、単体テストで Sleeper
の mock を使えるようになる。 Clock
が導入されたことで System.currentTimeMillis
を使っていた頃よりもテストしやすくなったのと似たような発想だ。
以下は Sleeper
を利用するクラスと、Mockito を使った単体テストの例。SomeService
に Sleeper
の mock を inject しているので、mock の sleep
メソッドが呼び出される。あとは、適切な引数とともに mock が呼び出されたことを検証すればよい。これで誤差に悩まされることはなくなるし、実際に指定された時間を消費することもない。
class SomeService(
private val sleeper: Sleeper,
) {
fun someMethod() {
sleeper.sleep(Duration.ofSeconds(5)) // It is equivalent to Thread.sleep(5000)
}
}
@ExtendWith(MockitoExtension::class)
class SomeServiceTest {
@InjectMocks
private lateinit var someService: SomeService
@Mock
private lateinit var sleeper: Sleeper
@Test
fun someMethodTest() {
someService.someMethod()
verify(sleeper).sleep(Duration.ofSeconds(5))
}
}
# まあ Thread.sleep
くらいなら PowerMock とかでもいいのではという話はある。あと Sleeper
自体のテストも PowerMock の出番になりそう。
本題からは外れるものの、Duration
を使えるようになったのも地味によかったと思う。