Assert実装のすゝめ

2024/09/07に公開

どういうコトか?

Rustで以下のような実装を行ったとしよう

pub enum Number{
    Integer(i64),
    Real(f64)
}

当然こいつに演算子のOverloadはしたくなるし、Fromも付けたくなる。今回は抜粋でAddだけ実装してみることにしよう。

pub enum Number{
	Integer(i64),
	Real(f64)
}

impl From<i64> for Number{
	fn from(value: i64) -> Self {
		todo!()
	}
}

impl From<f64> for Number{
	fn from(value: f64) -> Self {
		todo!()
	}
}
impl Add for Number{
	type Output = Number;

	fn add(self, rhs: Self) -> Self::Output {
		todo!()
	}
}

当然こいつに対するテストを書きたくもなる。


#[cfg(test)]
mod test{
	use super::*;
	#[test]
	fn from_i64() {
		let fixture=Number::from(42);
		assert!(matches!(&fixture,Number::Integer(i) if i==&42));
	}
}

こんな感じでassert!match!マクロを使うのが一般的だけど、このような文脈が高頻度に出てくるとき面倒だし、仮にこいつがそこそこ頻用されている場合、当然、Number型に対するAssertionも高頻度に発生するので、何となく楽できないかなと思った

要件

テスト専用とするなら、assertに失敗したらさっさと死んでくれて構わない。逆にそんなラフな実装を本番で使いたくはない。この2つをかなえる方法として、以下のようにテストモジュールと同様に#[cfg(test)]を付与してしまえば良いかなって

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

	impl Number{
		pub fn assert_as_integer(&self,expected:i64){
			assert!(matches!(self,Self::Integer(i) if i==&expected))
		}

		pub fn assert_as_float(&self,expected:f64){
			assert!(matches!(self,Self::Real(f) if f==&expected))
		}

		pub fn extract_as_i64(&self)->i64{
			match self {
				Number::Integer(i) => *i,
				_ => unreachable!()
			}
		}

		pub fn extract_as_f64(&self)->f64{
			match self {
				Number::Real(f) => *f,
				_ => unreachable!()
			}
		}
	}
}

今回は内容が即値なのでextractの必要性は薄いけど、例えばこれが入れ子構造だった場合はmatchマクロを使うより見通しは良くなる。

考慮事項

実装難易度はさほど高くなく、何ならCopilot当たりがうまく書いてはくれる程度のモノとは言え、たった1回しか使わないとかなら普通にassertとmatchマクロを用いた方が効率的では有る。ただ、対象が割と頻用されて、なおかつ他の型の戻り値なんかになっている場合は、この辺のコストをかけても作っておいた方が何かと便利なのかなって思います。

また、#[cfg(test)]を付与しているのでプロダクトバイナリにゴミが混入することもなく、加えて#[cfg(test)]以外で使おうとするとコンパイルエラーになるのでその点も安全です。

Discussion