$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

Scalaのテスト中にJoda-Timeのnowの時刻を固定する

今回はScalaのテストの話。時刻まわりが関わる実装をテストしたい時、テスト中だけ現在時刻を固定したり、現在時刻を過去にしたりなどといったことをやりたいことはよくある。Joda-TimeのDateTime.nowを使って現在時刻を取得している場合、時刻をfakeして固定することが出来たのでメモ。

固定するにはorg.joda.time.DateTimeUtilsのsetCurrentMillisFixedやsetCurrentMillisProviderを利用する。例えば、以下のように時間を固定することができる。

DateTimeUtils.setCurrentMillisFixed(15147648t00000L) // 2018-01-01に固定
println(DateTime.now) // 2018-01-01T09:00:00.000+09:00
DateTimeUtils.setCurrentMillisSystem() // 元に戻す
println(DateTime.now) // 2018-01-23T05:58:51.118+09:00


これだけでやりたいことは出来たのだけど、テスト用にはユーティリティを作っておくと、さらに便利になる。例えば、

import org.joda.time.{ DateTime, DateTimeUtils }
object TimeFaker {
  /**
   * 指定したtimeMillisに時刻を固定する。ブロックを抜けると元に戻る。
   */
  def fake[T](timeMillis: Long)(block: => T): T = {
    DateTimeUtils.setCurrentMillisFixed(timeMillis)
    try {
      block
    } finally {
      DateTimeUtils.setCurrentMillisSystem()
    }
  }

  /**
   * DateTimeオブジェクトを渡せるバージョン
   */
  def fake[T](t: DateTime)(block: => T): T =
    fake(t.getMillis)(block)

  /**
   * ISODateTimeFormatの形式で渡せるバージョン
   * 例) TimeFaker.fake("2018-03-02T12:34:56") { }
   */
  def fake[T](t: String)(block: => T): T =
    fake(DateTime.parse(t).getMillis)(block)
}

fakeを使うとブロック内だけ時刻が固定され、ブロックを抜けるとシステム時間に戻る。またブロック内で返却したものが結果として返ってくる。

// milliSecondsを渡せる
val result = TimeFaker.fake(1515974400000L) {
  println(DateTime.now) // 2018-01-01T09:00:00.000+09:00
  123
}
println(DateTime.now) // 時刻はもとに戻る
println(result) // 123

// DateTimeのオブジェクトを渡せる
val dt = new DateTime(2018, 2, 13, 14, 59)
TimeFaker.fake(dt) {
  println(DateTime.now) // 2018-02-13T14:59:00.000+09:00
}

// ISODateTimeFormatの形式で渡せる
TimeFaker.fake("2018-03-02T12:34:56") {
  println(DateTime.now) // 2018-03-02T12:34:56.000+09:00
}

便利!

ただしDateTimeUtilsを使った書き換えはスレッド共有の空間を書き換えるので、マルチスレッドでテストする時は動かないことに注意しましょう。