【Rust】Unit Testの実用的な書き方

use std::{fs::File, io::{BufReader, BufWriter}};
use serde::{Serialize, Deserialize};
use clap::{Parser, Subcommand};
use chrono::NaiveDateTime;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Schedule {
    id: u64,
    subject: String,
    start: NaiveDateTime,
    end: NaiveDateTime,
}
impl Schedule {
    fn intersects(&self, other: &Schedule) -> bool {
        self.start < other.end && other.start < self.end
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Calendar {
    schedules: Vec<Schedule>,
}

const SCHEDULE_FILE : &str = "schedule.json";

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        subject: String,
        start: NaiveDateTime,
        end: NaiveDateTime,
    }
}

fn main() {
    let options = Cli::parse();
    match options.command {
        Commands::List => show_list(),
        Commands::Add { subject, start, end }
            => add_schedule(subject, start, end),
    }
}

fn show_list() {
    let calendar : Calendar = {
        let file = File::open(SCHEDULE_FILE).unwrap();
        let reader = BufReader::new(file);
        serde_json::from_reader(reader).unwrap()
    };

    println!("ID\tSTART\tEND\tSUBJECT");
    for schedule in calendar.schedules {
        println!(
            "{}\t{}\t{}\t{}",
            schedule.id, schedule.start, schedule.end, schedule.subject
        );
    }
}

fn add_schedule(
    subject: String,
    start: NaiveDateTime,
    end: NaiveDateTime,
) {
    let mut calendar : Calendar = {
        let file = File::open(SCHEDULE_FILE).unwrap();
        let reader = BufReader::new(file);
        serde_json::from_reader(reader).unwrap()
    };

    let id = calendar.schedules.len() as u64;
    let new_schedule = Schedule {
        id,
        subject,
        start,
        end,
    };

    for schedule in &calendar.schedules {
        if schedule.intersects(&new_schedule) {
            println!("エラー: 予定が重複しています");
            return;
        }
    }
    calendar.schedules.push(new_schedule);

    {
        let file = File::create(SCHEDULE_FILE).unwrap();
        let writer = BufWriter::new(file);
        serde_json::to_writer(writer, &calendar).unwrap();
    }
    println!("予定を追加しました。");
}

#[cfg(test)]
mod tests {
    use super::*;

    fn naive_date_time(
        year: i32,
        month: u32,
        day: u32,
        hour: u32,
        minute: u32,
        second: u32,
    ) -> NaiveDateTime {
        chrono::NaiveDate::from_ymd_opt(year, month, day)
            .unwrap()
            .and_hms_opt(hour, minute, second)
            .unwrap()
    }

    fn test_schedule_intersects(
        h0: u32,
        m0: u32,
        h1: u32,
        m1: u32,
        should_intersects: bool
    ) {
        let schedule = Schedule {
            id: 0,
            subject: "既存予定1".to_string(),
            start: naive_date_time(2024, 1, 1, h0, m0, 0),
            end: naive_date_time(2024, 1, 1, h1, m1, 0),
        };
        let new_schedule = Schedule {
            id: 999,
            subject: "新規予定".to_string(),
            start: naive_date_time(2024, 1, 1, 19, 0, 0),
            end: naive_date_time(2024, 1, 1, 20, 0, 0),
        };
        assert_eq!(should_intersects, schedule.intersects(&new_schedule));
    }
    

    #[test]
    fn test_schedule_intersects_1(){
        test_schedule_intersects(18, 15, 19, 15, true)
    }

    #[test]
    fn test_schedule_intersects_2(){
        test_schedule_intersects(19, 45, 20, 45, true)
    }
}

$ cargo test
Compiling calendar v0.1.0 (/home/vagrant/dev/work/rusty/calendar)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.40s
Running unittests src/main.rs (target/debug/deps/calendar-867e0c10670d3913)

running 2 tests
test tests::test_schedule_intersects_2 … ok
test tests::test_schedule_intersects_1 … ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

なるほど、これはかなり参考になる。