13. テストを書く
🎯 この章のゴール
#[test]とassert!系マクロでユニットテストを書くcargo testの便利なオプションを知る- ユニットテストと結合テストの違いを理解する
💡 ひとことで言うと
Rust ではテストは 言語とツールに標準装備。追加ライブラリなしで、コードのすぐ隣にテストを書けます。
最初のテスト
cargo new --lib calc でライブラリを作ると、テストのひな形が入っています。
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn is_even(n: i32) -> bool { // 非公開関数
n % 2 == 0
}
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("ゼロ除算");
}
a / b
}
#[cfg(test)] // cargo test のときだけコンパイル
mod tests {
use super::*; // 親モジュールのアイテムを全部使う
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn private_fn_is_testable() {
assert!(is_even(4), "4 は偶数のはず");
assert_ne!(is_even(3), true);
}
#[test]
#[should_panic(expected = "ゼロ除算")]
fn divide_by_zero_panics() {
divide(1, 0);
}
#[test]
fn using_result() -> Result<(), String> {
if add(1, 1) == 2 { Ok(()) } else { Err("計算がおかしい".into()) }
}
}$ cargo test
running 4 tests
test tests::adds_two_numbers ... ok
test tests::private_fn_is_testable ... ok
test tests::divide_by_zero_panics - should panic ... ok
test tests::using_result ... ok
test result: ok. 4 passed; 0 failedアサーションマクロ
| マクロ | 成功条件 |
|---|---|
assert!(cond) | cond が true |
assert_eq!(a, b) | a == b(失敗時に両方の値を表示) |
assert_ne!(a, b) | a != b |
どれも末尾にメッセージ(format! と同じ書式)を追加できます。
cargo test の便利オプション
cargo test adds # 名前に "adds" を含むテストだけ実行
cargo test -- --nocapture # println! の出力を表示
cargo test -- --ignored # #[ignore] を付けた重いテストを実行
cargo test --doc # ドキュメントテストだけ結合テスト
calc/
├── src/lib.rs
└── tests/
└── api.rs// tests/api.rs — 外部のユーザーと同じ立場でテスト
use calc::add;
#[test]
fn add_from_outside() {
assert_eq!(add(10, 20), 30);
}
///で書くドキュメントコメント内のコード例もcargo testで実行されます(ドキュメントテスト)。コードを変更してコード例が動かなくなると、テストが失敗するので気づけます。
🧪 やってみよう
下の書きかけコードの TODO を埋めて、▶ 実行 で確かめよう。todo!() のままだと実行時に panic します。
平均値を返す関数にテストを書いてみよう。▶ 実行(テスト) で cargo test 相当が走ります。
pub fn mean(v: &[i32]) -> Option<f64> {
if v.is_empty() {
return None;
}
Some(v.iter().sum::<i32>() as f64 / v.len() as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mean_of_values() {
todo!("assert_eq!(mean(&[1, 2, 3]), Some(2.0)) のように確かめよう")
}
#[test]
fn mean_of_empty() {
todo!("空のときは None")
}
}💡 解答例を見る
pub fn mean(v: &[i32]) -> Option<f64> {
if v.is_empty() {
return None;
}
Some(v.iter().sum::<i32>() as f64 / v.len() as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mean_of_values() {
assert_eq!(mean(&[1, 2, 3]), Some(2.0));
assert_eq!(mean(&[1, 2]), Some(1.5));
}
#[test]
fn mean_of_empty() {
assert_eq!(mean(&[]), None);
}
}📝 まとめ
#[cfg(test)] mod tests+#[test]でユニットテストassert!/assert_eq!/#[should_panic]tests/ディレクトリで公開 API の結合テスト
✅ 理解度クイズ
Q1
関数をテストとして認識させるために付ける属性は?
#[test] を付けた関数が cargo test で実行されます。#[cfg(test)] は「テスト時だけコンパイルする」ためにモジュールに付けます。
Q2
「この関数は panic するはず」をテストするには?
#[should_panic](必要なら expected = "メッセージの一部")を付けると、panic したときに成功になります。
Q3
結合テスト(integration test)を置くディレクトリは?
プロジェクト直下の tests/ 内の各ファイルは独立したクレートとしてコンパイルされ、ライブラリの 公開 API だけを使ってテストします。
Q4
ユニットテストから非公開関数をテストできる?
mod tests は子モジュールなので、use super::*; で親の非公開関数も使えます。