🕰️

Rustで時間の関わるコードを簡単にテストする

に公開

ちゃんとやる場合はchrono::TimeZoneを実装すると思うが手間なので、自分の書く範囲であれば次のようにnow() -> chrono::DateTime<chrono::Utc>を持つ別物のmock_chrono::Utcを置くのが良さそう。

https://stackoverflow.com/a/76747577/4593717

時分秒を指定したりしてセットできると便利だろう。

#[cfg(test)]
mod mock_chrono {
    use chrono::{DateTime, NaiveDate};
    use std::cell::Cell;

    thread_local! {
        static TIMESTAMP: Cell<i64> = const { Cell::new(0) };
    }

    pub struct Utc;

    impl Utc {
        pub fn now() -> DateTime<chrono::Utc> {
            TIMESTAMP
                .with(|timestamp| DateTime::<chrono::Utc>::from_timestamp(timestamp.get(), 0))
                .expect("invalid timestamp")
        }
    }

    pub fn set_timestamp(h: u32, m: u32, s: u32) {
        let timestamp = NaiveDate::from_ymd_opt(2025, 1, 1)
            .unwrap()
            .and_hms_opt(h, m, s)
            .unwrap()
            .and_utc()
            .timestamp();
        TIMESTAMP.with(|ts| ts.set(timestamp));
    }
}

テスト用なのでunwrap()祭り。

あとは状況にも依るが、now()以降ではchrono::Utcを扱うわけなので、一々chrono::Utcと書くよりはnow()を呼ぶ時だけ条件付きコンパイルするのが綺麗ではなかろうか。

use chrono::{DateTime, Utc};

fn current_time() -> DateTime<Utc> {
    #[cfg(test)]
    let now = mock_chrono::Utc::now();
    #[cfg(not(test))]
    let now = Utc::now();
    now
}

あまりモックという感じではなくなるけれども。

Discussion