項目30:ユニットテスト以上のものを書こう
https://effective-rust.com/testing.html
LT;DR
private なコードを含む個々の機能を細かくテストするには、単体テスト を書く
cargo test で実行する
public な API をテストするには、統合テスト を書く
cargo test で実行する
各 public な API の使い方を例示するには、ドキュメンテーションテスト を書く
cargo test で実行する
public な API 全体の使い方を例示するには、サンプルコードを書く
cargo test --examples または cargo run --example <name> で実行する
コードの パフォーマンス が重要ならば、ベンチマークを測定する
cargo bench で実行する
信頼できない入力値が渡ってくるコードには、ファズテスト を書く
cargo fuzz で実行する
hr.icon
単体テスト
単体テストコードは、独立したモジュールに書く
モジュール名は慣習的に test または tests
モジュールの定義は実装ファイルと同じか、別のファイルを作成しても良い
テストモジュールでは、テスト対象の親モジュールのすべてを ワイルドカード(use super::*)を用いても良い
項目23:ワイルドカードインポートを避けよう の例外
テストモジュールは親モジュールの内容を 可視性 に関わらず利用可能
テストの失敗には panic! を用いるので、テストコードでは expect や unwrap を用いても良い
例えば、 assert! や assert_eq! は失敗すると panic を起こす
項目18:Don't panic は当てはまらない
テスト対象が panic を起こす可能性がある場合は、テスト関数に #[should_panic] を付ける
型システムで保証されていることをテストする必要はない
依存クレートの機能を用いているコードはテストを書くべき
SemVer では表現されない API のシグネチャの変化以外の 必要とする機能が変化したことに気づくため
統合テスト
統合テストは tests/ に配置する
このディレクトリ内の各ファイルは #[test] が付与されたすべての関数を実行する
private なアイテムにはアクセスできないので、public な API だけをテストする
ドキュメンテーションテスト
項目27:パブリックインターフェイスのドキュメントを書こう
cargo test 実行時、ドキュメンテーションコメント 内の個々のコードは暗黙的に fn main() {...} でラップされて実行される
cargo test --doc <item-name> のように関数名を指定して実行することも可能
CI で定常的にテストを実行すれば、サンプルコードと API が乖離することを防ぐことができる
関連: コメントはコードの近くに配置すること
サンプルコード
サンプルコードは examples/ に配置する
このディレクトリ配下の 個々のファイル や main.rsを含むサブディレクトリ は独立した バイナリ となる
実行するには cargo run --example <name> または cargo test --example <name> で可能
サンプルコードはクレートの public API にしかアクセスできない
CI で定期的にビルド・実行(cargo test --examples)することで、リグレッション に気が付けるようにすると良い
ユーザがコピペして利用することを考慮して unwrap メソッドを使わないようにし、main 関数の戻り値も Result<(), Box<dyn Error>> として常に ? を用いるようにした方が良い
ベンチマーク
ベンチマークを実行するには、cargo bench を実行する
warning.icon 実際にベンチマークを測定するには、安定化していない API(e.g. std::test::Bencher)を利用する必要があるため、cargo +nightly bench を実行する必要がある
std::test::Bencher の代替として criterion を使っても良い
CI で定常的に実行することで、コードやツールチェインの変更によって パフォーマンス に悪影響をいち早く検知できる
warning.icon コンパイル最適化 によって稼働時の値よりも良い結果が得られることがある
Godbolt compiler explorer を用いてコンパイラが出力した実際の アセンブリ コードを確認すると、最適化を確認できる
e.g. 階乗 を求める関数のベンチマーク
コード
code:rs
#!feature(test)
extern crate test;
pub fn factorial(n: u128) -> u128 {
match n {
0 => 1,
_ => n * factorial(n - 1),
}
}
#cfg(test)
mod test {
use super::*;
#bench
fn bench_factorial(b: &mut test::Bencher) {
b.iter(|| {
let result = factorial(15);
assert_eq!(result, 1_307_674_368_000);
});
}
}
実行
code:sh
$ cargo +nightly bench
running 1 test
test tests::bench_factorial ... bench: 0.42 ns/iter (+/- 0.00)
原因
以下の 2 つの理由により、コンパイラは最適化でループをすべて取り除いて、結果だけを返すコードに置き換えるため
テスト対象のコードに渡される引数が決まっていて実行時に変更されている
テスト対象のコード量が少ない
入力値に 恒等関数(std::hint::black_box)を渡すと、コンパイラに最適化を抑制するように促すことができる
warning.icon ただし、実際に最適化が抑制されるかは、コンパイラの判断に委ねられる
e.g.
code:rs
#bench
fn bench_factorial(b: &mut test::Bencher) {
b.iter(|| {
let result = factorial(std::hint::black_box(15));
assert_eq!(result, 1_307_674_368_000);
});
}
再実行
code:sh
$ cargo +nightly bench
running 1 test
test tests::bench_factorial ... bench: 14.72 ns/iter (+/- 0.48)
ファズテスト
「潜在的な攻撃者にコードが晒されるなら、ファズテストを行おう」
ファズテストは メモリ安全性 を保証する際に利用されることが多いため Rust には必要ないと思われがち
しかし、バグをすべて防げるわけではないし、panic を起こす コードパス があれば、コード全体に対する DoS 攻撃 が成立する
Rust コンパイラは LLVM を用いて構築されているため、cargo-fuzz を用いれば、Rust で libFuzzer の機能を利用できる
ファズテストを行う手順(Rust Fuzz Book)
1. 任意のバイト列を入力として受け取る関数を見つける
code:rs
pub fn is_fuzz(data: &u8) -> bool {
if data.len() >= 3
&& data0 == b'F'
&& data1 == b'U'
&& data2 == b'Z'
&& data3 == b'Z'
{
true
} else {
false
}
}
2. cargo fuzz init でファズテストのサブプロジェクトを作成する
作成に成功すると、以下のような fuzz ディレクトリが作成される
code:_
fuzz/
├── Cargo.toml
└── fuzz_targets
└── fuzz_target_1.rs
3. fuzz_target! 内でテスト対象の関数を呼び出す
code:fuzz/fuzz_targets/fuzz_target_1.rs
#!no_main
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &u8| {
let _ = some_crate::is_fuzz(data);
});
4. cargo +nightly fuzz run <fuzz target name> を実行する
e.g. cargo +nightly fuzz run fuzz_target_1
クラッシュするまで、ランダムなデータを用いてファズテストを実行し続ける
通常、ファズテストは問題を見つけるまでに時間を要するため、CI 上で実行すべきではない
代わりに、新しいリリースや大きな変更がある場合に限定して、一定期間だけ実行するのが良い
ファズテストの効率を上げるには、新しいコードパスを発見した際の入力を保存して再利用 すると良い
これにより、以降のテストでは既存のコードパスを再テストせずに、新しいコードパスの探索に注力できる
テストに関するアドバイス
「変更するたびに CI ですべてのテストを実行しよう」(ファズテストは除く)
バグを修正する際には、「バグを顕在させるテストを修正前に書こう」
これにより、バグが修正されたことも確信でき、将来バグが誤って再現することも無くなる
クレートに フィーチャ がある場合「フィーチャのすべての可能な組み合わせに対してテストしよう」
#[cfg(target_os = "...")] が存在する場合、「すべてのプラットフォームでテストを実行しよう」
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目