Rustにおいて、堅牢なソフトウェアを開発するために欠かせない要素がテストです。
Rustは言語レベルでテスト機能を強力にサポートしており、標準ツールであるcargoを使用することで、誰でも簡単に信頼性の高いコードを維持できるよう設計されています。
本記事では、Rustにおけるテストの基本から、実務で役立つ応用的なcargo testの使い方、さらにはテストの実行効率を最大化するための高度なオプションまでを詳しく紐解いていきます。
Rustにおけるテストの基本構造
Rustのテストは、開発者が書いたコードが意図通りに動作することを検証するプログラムです。
Rustでは、テストコードをプロダクトコードと同じファイルに記述する「ユニットテスト」と、外部のクレートとして利用するユーザーの視点で記述する「結合テスト」の2種類を使い分けることが一般的です。
ユニットテストの記述方法
ユニットテストは、各モジュールの内部的なロジックを検証するために使用されます。
通常、ソースファイルの下部にmod testsというモジュールを作成し、そこにテスト関数を記述します。
// lib.rs または main.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
// 親モジュールの関数を使えるようにする
use super::*;
#[test]
fn test_add() {
// assert_eq!マクロで期待値と実際の値を比較
assert_eq!(add(2, 2), 4);
}
}
ここで重要なのは#[cfg(test)]アトリビュートです。
これは、「cargo testを実行したときのみ、このモジュールをコンパイルする」という指示をコンパイラに与えます。
これにより、通常のビルド(cargo build)時にはテストコードが含まれず、バイナリサイズやコンパイル時間を最適化できる仕組みになっています。
アサーションマクロの活用
テスト結果を判定するために、Rustでは主に3つのマクロが用意されています。
| マクロ名 | 用途 |
|---|---|
| assert!(条件) | 条件が true であることを確認する。 |
| assert_eq!(左辺, 右辺) | 2つの値が等しいことを確認する。失敗時に両方の値を表示する。 |
| assert_ne!(左辺, 右辺) | 2つの値が等しくないことを確認する。 |
これらのマクロは、条件を満たさない場合にパニック(panic)を引き起こし、そのテストが失敗したことをcargo testに通知します。
効率的なテスト作成のためのテクニック
テストの数が増えてくると、単に「動くかどうか」を確認するだけでなく、「どのように効率よく、網羅的にテストするか」が重要になります。
異常系のテスト:should_panic
プログラムがエラーに対して正しく反応するか、つまり「適切にパニックするか」をテストしたい場合があります。
その際には#[should_panic]アトリビュートを使用します。
pub fn check_positive(val: i32) {
if val < 0 {
panic!("Value must be positive!");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Value must be positive!")]
fn test_panic_on_negative() {
check_positive(-1);
}
}
expected引数を使用することで、特定のパニックメッセージが含まれているかまで検証できます。
これにより、意図しない別の理由でパニックが発生した場合にテストが通ってしまうのを防ぐことができます。
Result型を返すテスト
Rustのテスト関数は、パニックさせるだけでなく、Result<(), E>型を返すよう記述することも可能です。
これにより、テスト内で?演算子(クエスチョンマーク演算子)を活用でき、エラー処理を伴うコードのテストが非常に簡潔になります。
#[test]
fn test_with_result() -> Result<(), String> {
let result = some_fallible_function()?;
if result == 10 {
Ok(())
} else {
Err(String::from("Value should be 10"))
}
}
このスタイルは、エラーチェーンを扱うモダンなRust開発において非常に推奨されるパターンです。
結合テストによる外部インターフェースの検証
ユニットテストが個別の部品を検査するのに対し、結合テストは複数のライブラリ機能が正しく組み合わさって動作するかを確認します。
testsディレクトリの活用
結合テストは、プロジェクトのルートディレクトリにtests/ディレクトリを作成し、その中に.rsファイルを配置することで作成します。
Cargoはこのディレクトリ内の各ファイルを独立したクレートとしてコンパイルします。
// tests/integration_test.rs
use my_crate::add;
#[test]
fn test_external_add() {
assert_eq!(add(5, 5), 10);
}
結合テストでは、公開されている(pub)APIのみにアクセス可能です。
これにより、ライブラリの利用者が体験する実際の挙動を再現することができます。
ドキュメンテーションテスト:ドキュメントを「生かす」
Rustの非常に強力な機能の一つに「ドキュメンテーションテスト」があります。
これは、ドキュメントコメント(///)内に記述されたコード例を、実際のテストとして実行する機能です。
/// 二つの数値を足し合わせます。
///
/// # Examples
///
/// ```
/// let res = my\_crate::add(1, 2);
/// assert\_eq!(res, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
ドキュメント内のコードが古くなり、現在の実装と乖離してしまう現象(ドキュメントの腐敗)を防ぐことができます。
cargo testを実行すると、これらのコード例も自動的にコンパイル・実行されます。
高度な cargo test 実行オプション
テストコードが増大すると、すべてのテストを実行するのに時間がかかるようになります。
また、特定のデバッグを行いたい場合には標準の出力設定では不十分なこともあります。
ここでは、cargo testの挙動を制御する高度なオプションを紹介します。
並列実行の制御
デフォルトでは、cargo testはスレッドを使用してテストを並列に実行します。
しかし、テストがファイルシステムやデータベースなどの共有リソースに依存している場合、競合が発生してテストが不安定になる(Flaky Test)ことがあります。
このような場合、--test-threadsオプションを使用して並列数を制限できます。
# テストを1スレッドずつ順番に実行する
cargo test -- --test-threads=1
注意点: cargo test に渡す引数と、その下のテストバイナリに渡す引数を区別するために、--(ダッシュ2つ)が必要です。
標準出力の表示(–nocapture)
Rustのテストランナーは、デフォルトでは成功したテストの標準出力をキャプチャして非表示にします。
デバッグのために println! の結果を常に確認したい場合は、--nocaptureフラグを使用します。
cargo test -- --nocapture
これにより、テストの成功・失敗に関わらず、すべての標準出力がターミナルに表示されるようになります。
特定のテストのみを実行する
プロジェクト規模が大きくなると、特定のモジュールや特定の関数名を含むテストだけを素早く実行したい場面が増えます。
# 関数名に "test_add" を含むテストのみ実行
cargo test test_add
# 特定のモジュール内のテストのみ実行
cargo test tests::mod_name
また、#[ignore] アトリビュートを付けた「通常は実行したくない重いテスト」だけを明示的に実行することも可能です。
#[test]
#[ignore]
fn heavy_computation_test() {
// 実行に数分かかるような処理
}
# ignoreされたテストのみを実行
cargo test -- --ignored
Rustテストのベストプラクティス
2026年現在の開発環境において、Rustのテストをより効果的に運用するためのポイントをまとめます。
テストコードのDRY原則にこだわりすぎない:
プロダクトコードでは共通化が推奨されますが、テストコードでは「そのテストが何を検証しているか」がひと目で分かることが優先されます。多少の重複があっても、読みやすさを重視しましょう。private関数のテスト:
Rustでは子モジュールから親モジュールの非公開(private)関数にアクセスできるため、mod tests内でユニットテストを書くことで、非公開関数の動作確認が容易に行えます。CIでの自動化:
GitHub ActionsなどのCIツールで、プルリクエストごとに必ずcargo testを実行する設定を入れましょう。この際、--lockedフラグを付けて依存関係のバージョンを固定するのが定石です。プロパティベーステストの検討:
特定の入力値だけでなく、ランダムな大量の入力値でバグを探す手法です。Rustではproptestクレートなどが有名です。特に、複雑なアルゴリズムやパース処理を書く際に威力を発揮します。
まとめ
Rustの cargo test は、単なるコマンド以上の価値を提供します。
それは「コードが正しいこと」を保証するための強力なフレームワークであり、開発者の意図を未来にわたって維持するためのツールです。
本記事で解説したユニットテストと結合テストの使い分け、アサーションの活用、そして高度なコマンドラインオプションをマスターすることで、Rust開発の生産性は飛躍的に向上します。
「テストを書きやすい」というRustの特性を最大限に活かし、自信を持ってコードをデプロイできる環境を構築していきましょう。
日々の開発において、まずは小さな関数からテストを書く習慣をつけることが、結果として保守性の高い、美しいコードベースを育てる近道となります。
