Cover Image for [Rust] test時に依存するメソッドをmockする方法を考える

[Rust] test時に依存するメソッドをmockする方法を考える

動機

use chrono::{DateTime, Local};  
  
fn get_unix_time_in_local() -> i64 {  
    let local_datetime: DateTime<Local> = Local::now();  
    local_datetime.timestamp()  
}  
  
#[cfg(test)]  
mod tests {  
    use super::*;  
  
    #[test]  
    fn can_get_unix_time() {  
        let actual = get_unix_time_in_local();  
        assert_eq!(actual, 0); // エラーになる。現在時刻に依存するからどうやってテストする????  
    }  
}

雑だがこんなふうに、別のメソッドに依存したロジックをrustでどうテストするのか。TypeScriptとかNode.jsに飼い慣らされた私の脳だとmockしてしまえばいいじゃんと思うが、rust だと overload / override どちらもできないので単純に mock するのは難しい

解決策

use chrono::{DateTime, Local};  
  
fn get_unix_time(get_now: impl Fn() -> DateTime<Local>) -> i64 {  
    let local_datetime: DateTime<Local> = get_now();  
    local_datetime.timestamp()  
}  
  
fn get_unix_time_in_local() -> i64 {  
    let get_now = || Local::now();  
    get_unix_time(get_now)  
}  
  
#[cfg(test)]  
mod tests {  
    use super::*;  
    use chrono::{FixedOffset, NaiveDate, NaiveDateTime, NaiveTime};  
  
    #[test]  
    fn can_get_unix_time() {  
        let dummy_date: DateTime<Local> = DateTime::from_utc(  
            NaiveDateTime::new(  
                NaiveDate::from_ymd(2023, 4, 1),  
                NaiveTime::from_hms(12, 59, 0),  
            ),  
            FixedOffset::east(9 * 3600),  
        );  
  
        let mock_fn = || dummy_date;  
        let actual = get_unix_time(mock_fn); // I/F を変更して現在時刻を取得する関数を渡せるようにする  
        assert_eq!(actual, dummy_date.timestamp());   
}  
}

関数型的な考え方だと元の実装だと現在時刻を取得する処理に依存しているという部分が、表現できてないからテストが難しいのだと思う。そのため、現在時刻を取得する処理を外から注入できるようにする。 testではそのメソッドにダミーで作ったメソッドを注入することでテストできるようにする

多分だが課題としては、mockすることに固執しすぎてなぜテストしづらいのかを考えなかったことだと思う。mockしづらいのではなくて、依存関係を適切なI/Fとして表現できていないことがテストのしづらさにつながっているのではないかと思う。