2021/10/19

rustで単体テストをSerialに(同一スレッドで)実行する

rust

rustでテストケースを複数実行すると、意図せずエラーになる場合があります。

これは複数のテストケースが同時に実行されるからで、例えば環境変数を使うような場合や、同一のデータベースを使うような場合など
スレッドを跨いで同じリソースを使うようなテストケースをまとめて実行するケースでエラーになってしまいます。

これを解決するのが serial_test crateです。(すごい便利でした。)

テストの関数に#[serial]macroを宣言すれば良いだけで使えるのが、とても便利でした。

詳細な使い方は以下のようになります。(コメントでインラインに記載しています)

how to install

[dev-dependencies]
serial_test = "0.5.1"  # この行を追加

how to use

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
    pub host: String,
    pub project_id: String,
    pub project_key: String,
    pub environment: Option<String>,
    pub app_version: Option<String>,
    pub user_agent: String,
}

fn default_user_agent() -> String {
    format!("{}-{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
}

impl Default for Config {
    fn default() -> Self {
        let host = std::env::var("AIRBRAKE_HOST").unwrap_or("https://api.airbrake.io".to_owned());
        let project_id = std::env::var("AIRBRAKE_PROJECT_ID").unwrap_or("0".to_owned());
        let project_key = std::env::var("AIRBRAKE_API_KEY").unwrap_or("0".to_owned());
        let environment = std::env::var("AIRBRAKE_ENVIRONMENT").map(|inner| Some(inner)).unwrap_or(None);
        Self {
            host,
            project_id,
            project_key,
            environment,
            app_version: None,
            user_agent: default_user_agent(),
        }
    }
}

impl Config {
    pub fn endpoint(&self) -> String {
        format!(
            "{}/api/v3/projects/{}/notices",
            self.host,
            self.project_id,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::Config;
    use serial_test::serial;  // ★★★ ここ

    #[test]
    #[serial]  // ★★★ ここ
    fn test_default_config() {
        std::env::remove_var("AIRBRAKE_HOST");
        std::env::remove_var("AIRBRAKE_PROJECT_ID");
        std::env::remove_var("AIRBRAKE_API_KEY");
        std::env::remove_var("AIRBRAKE_ENVIRONMENT");
        let config = Config::default();
        let expected = Config {
            host: "https://api.airbrake.io".to_owned(),
            project_id: "0".to_owned(),
            project_key: "0".to_owned(),
            environment: None,
            app_version: None,
            user_agent: "errbit-0.1.0".to_owned(),
        };
        assert_eq!(expected, config);
        assert_eq!(
            "https://api.airbrake.io/api/v3/projects/0/notices",
            config.endpoint()
        );
    }

    #[test]
    #[serial]  // ★★★ ここ
    fn test_config() {
        std::env::set_var("AIRBRAKE_HOST", "https://errbit.example.com");
        std::env::set_var("AIRBRAKE_PROJECT_ID", "1");
        std::env::set_var("AIRBRAKE_API_KEY", "my-key");
        std::env::set_var("AIRBRAKE_ENVIRONMENT", "dev");
        let config = Config::default();
        let expected = Config {
            host: "https://errbit.example.com".to_owned(),
            project_id: "1".to_owned(),
            project_key: "my-key".to_owned(),
            environment: Some("dev".to_owned()),
            app_version: None,
            user_agent: "errbit-0.1.0".to_owned(),
        };
        assert_eq!(expected, config);
        assert_eq!(
            "https://errbit.example.com/api/v3/projects/1/notices",
            config.endpoint()
        );
    }
}