monolithic kernel

Thread.sleep を wrap してテストを書きやすくする

September 25, 2020

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 を使った単体テストの例。SomeServiceSleeper の 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 を使えるようになったのも地味によかったと思う。