paild tech blog

法人カードのクラウド型発行・管理サービスpaild

claim クレートを利用してテスト時のコードをスッキリさせる

お手伝いの yuki です。今日はクレートを使った小ネタです。claim という、アサーション関連の便利なマクロを提供するクレートがあります。意外と知られていない気もするので紹介しておこうと思います。

claim

assert_matches! や Result の結果を判定するのに便利な assert_ok! 、Option の結果を判定するのに便利な assert_some! 、非同期処理周りで用いられる Poll の準備完了判定を行える assert_ready! などの便利なアサーションに関連するマクロを提供するクレートです。シンプルながらも強力なクレートです。

下記から利用することができます。

github.com

今回はテスト用として説明しますが、もちろん本流の処理にも利用できます。リリースビルド時には挿入されない debug_* で始まるマクロも、すべてのマクロに対して提供されています。

加えて #![no_std] 環境でも動きます。

使ってみる

下記で依存を追加できます。

$ cargo add --dev claim

ないしは、 Cargo.toml に下記を追加します。

[dev-dependencies]
claim = "0.5.0"

今回は適当なサンプルプロジェクトを立ち上げて試してみます。

$ cargo new claim-example --lib

適当に、ある団体のメンバー管理のデータをたくさん実装します。例示のためにいくつかのパターンを用意しておきます。

use std::collections::HashMap;

#[derive(Debug, PartialEq, Eq)]
pub struct Member {
    name: MemberName,
}

#[derive(Debug, PartialEq, Eq)]
pub struct MemberName(String);

impl TryFrom<String> for MemberName {
    type Error = MemberError;

    fn try_from(name: String) -> Result<Self, Self::Error> {
        if name.len() == 0 {
            Err(MemberError::NameShouldNotBeEmpty)
        } else if name.len() > 24 {
            Err(MemberError::NameShouldLessThanEqual24)
        } else {
            Ok(Self(name))
        }
    }
}

#[derive(Debug)]
pub enum MemberError {
    NameShouldNotBeEmpty,
    NameShouldLessThanEqual24,
}

pub struct Cache(HashMap<String, Member>);

今回検証しつつ claim の使い方を確認したいのは下記のパターンです。

  1. Result 型に対する判定。
  2. パターンマッチングが必要なものに対する判定。
  3. Option 型に対する判定。

Result 型に対する判定

MemberName には意図的にバリデーションチェックを付与してあります。空の文字列を入れようとした場合と、入れた文字列が24文字以下でなかった場合にエラーを返すようになっています。

たとえば空の文字列に対するテストを書いてみましょう。通常であれば次のように書かれるはずです。

    #[test]
    fn raise_error_if_name_is_empty() {
        let name = "".to_string();
        let result = MemberName::try_from(name);
        assert!(result.is_err());
    }

これでも悪くはないのですが、claim を利用すると下記のように書くことができます。

    #[test]
    fn raise_error_if_name_is_empty() {
        let name = "".to_string();
        let result = MemberName::try_from(name);
        assert_err!(result);
    }

パターンマッチングが必要なものに対する判定

一番便利さが顕著な例は、返されるエラーの値を検証したい場合でしょうか。通常であれば matches! マクロなどを利用して迂回しながら書く必要がありますが[*1]assert_matches! マクロ一つで済みます。

    #[test]
    fn raise_specific_error_if_name_len_is_greater_than_equal_25() {
        let name = "a".repeat(25);
        let result = MemberName::try_from(name);
        assert_matches!(result, Err(MemberError::NameShouldBeLessThanEqual24));
    }

便利ですね。

Option 型に対する判定

Result 型と同様に Option 型もテスト時にチェックしたいことは多いです。やはり同様に確認することができます。

assert_some! を利用すると、結果が Some かどうかを判定できます。Noneassert_none! マクロで確認可能です。assert_some_eq! で、Some かつその結果が期待したものと一致するかを確認できます。

    #[test]
    fn cache_works() {
        let dummy = HashMap::from_iter(vec![
            (
                "id-1".to_string(),
                Member {
                    name: MemberName::try_from("Socrates".to_string()).unwrap(),
                },
            ),
            (
                "id-2".to_string(),
                Member {
                    name: MemberName::try_from("Plato".to_string()).unwrap(),
                },
            ),
            (
                "id-3".to_string(),
                Member {
                    name: MemberName::try_from("Aristotle".to_string()).unwrap(),
                },
            ),
        ]);
        let cache = Cache(dummy);
        assert_some!(cache.0.get("id-1"));
        assert_some_eq!(
            cache.0.get("id-2"),
            &Member {
                name: MemberName::try_from("Plato".to_string()).unwrap(),
            }
        );
        assert_none!(cache.0.get("id-4"));
    }

まとめ

手軽に入れられて記述量を減らせるのでいいマクロだと思います。みなさんもぜひ!

*1:ちなみにですが、assert_matches! マクロ自体は提案はされているものの現状 unstable な機能です: https://github.com/rust-lang/rust/issues/82775