Rust のテストで動作を証明する

RustBeginner
オンラインで実践に進む

はじめに

テストは、期待する動作を実行可能な証拠に変えます。焦点を絞ったテストでは、1 つの動作を呼び出し、実際の結果と期待する結果を比較します。さらに、将来の変更によるリグレッションを自動的に検出できるため、変更を安全に行えます。

この実験では、準備済みの配送見積もりライブラリにテストを追加します。本番用の関数は意図的に小さくしてあります。そのため、Rust のテストをどこに配置するか、アサーションで期待する動作をどのように表現するか、どの境界ケースが重要か、Cargo で一度に 1 つのテストターゲットを実行する方法に集中できます。

焦点を絞ったユニットテストを追加する

このステップでは、ライブラリコードの近くにユニットテストを配置し、指定した名前のテストだけを実行します。

プロジェクトは /home/labex/project/shipping-quote にあります。まず、このディレクトリに移動します。

cd /home/labex/project/shipping-quote

編集する前に、短いライブラリのソースを確認します。ここで使う sed -n '1,220p' は、読み取り専用の表示コマンドです。-n は自動表示を抑制し、1,220p は 1 行目から 220 行目までを明示的に表示します。この範囲で、エディターを開かずにファイル全体を確認できます。

sed -n '1,220p' src/lib.rs

#[...] の形式で書かれた構文は attribute(アトリビュート) です。これは、その直後にある Rust の項目に付加するメタデータです。#[cfg(test)] アトリビュートは、後続のテストモジュールをテストビルド時だけコンパイルするよう Rust に指示します。モジュールは、関連する項目をまとめる名前付きのコンテナーです。次の Guided Lab では、一般的な整理方法としてモジュールを扱います。

mod tests の中では、パスにある super は親モジュールを指し、* はその親モジュールからアクセス可能なすべての名前を指します。したがって、use super::*; によって、テストモジュール内でライブラリの関数を短い名前で利用できます。最後に、#[test] が付いた関数は、Cargo が検出して実行できるテストになります。

Nano でソースを開きます。

nano src/lib.rs

// FIRST_UNIT_TEST を次のテストに置き換えます。

    #[test]
    fn light_parcel_costs_five() {
        assert_eq!(shipping_cost(3), 5);
    }

assert_eq! は 2 つの式を比較します。等しい場合、テストはそのまま続行します。等しくない場合、テストは失敗し、両方の値を表示します。ここでは、3 キログラムの荷物の配送費が 5 単位になることをテストとして記録しています。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。cargo test の後にテスト名を指定すると、その文字列を名前に含むテストだけを実行できます。

cargo test light_parcel_costs_five

Cargo はテストビルドをコンパイルし、指定したユニットテストの結果を表示します。

test tests::light_parcel_costs_five ... ok

test result: ok. 1 passed; 0 failed; ...

tests:: という接頭辞は、このテストがライブラリ内のローカルテストモジュールにあることを示します。ok と表示されれば、アサーションが成功したことを意味します。

境界ケースをカバーする

このステップでは、配送動作が変わる境界でテストを追加します。

3 のような代表値を使うと、軽量荷物の範囲内にある 1 つの値を検証できます。しかし、バグは境界に潜んでいることがよくあります。準備済みの match にある 1..=5range pattern(範囲パターン) です。これは 1 から 5 までのすべての値に一致し、両端の値も含みます。ループで使う包括的な範囲の表記を、新しいパターンの文脈で利用しています。この関数は 0 のときに特別な動作をし、ちょうど 5 キログラムまでは 5 単位の料金を維持します。これらの値をテストすることで、両方の境界を記録できます。

もう一度ライブラリを開きます。

nano src/lib.rs

// EDGE_UNIT_TESTS を次の 2 つのテストに置き換えます。

    #[test]
    fn empty_parcel_is_free() {
        assert_eq!(shipping_cost(0), 0);
    }

    #[test]
    fn five_kilograms_is_still_light() {
        assert_eq!(shipping_cost(5), 5);
    }

各テストでは、名前で 1 つのルールを説明し、焦点を絞ったアサーションを 1 つだけ記述しています。そのため、失敗した場合に原因を簡単に特定できます。Nano で保存して終了します。

tests:: フィルターは、ローカルのユニットテストモジュールにあるテストの完全な名前に一致します。これを使うと、別の統合テストターゲットをいったん除外し、3 つのユニットテストをすべて実行できます。

cargo test tests::

3 つのユニットテストが成功すれば、次のように表示されます。

test tests::empty_parcel_is_free ... ok
test tests::five_kilograms_is_still_light ... ok
test tests::light_parcel_costs_five ... ok

test result: ok. 3 passed; 0 failed; ...

これらの例によって、通常の値と、軽量荷物ルールにおける 2 つの重要な境界をカバーできます。

統合テストを完成させる

このステップでは、別のファイルからライブラリの公開インターフェースを通してテストします。

src/lib.rs 内のユニットテストは実装の近くにあり、親モジュールの内部項目を利用できます。一方、統合テストはトップレベルの tests/ ディレクトリに配置します。統合テストは独立したテストプログラムとしてコンパイルされ、外部の呼び出し元と同じように、ライブラリの公開インターフェースだけを使用します。パッケージ名 shipping-quote は、ソースコード内ではアンダースコアを使った Rust のクレート名 shipping_quote になります。次の Lab では、パッケージ、クレート、モジュール、公開可視性に関する用語を整理します。

準備済みの統合テストのひな形を確認します。

sed -n '1,160p' tests/order_workflow.rs

このファイルでは、公開関数 order_total だけをインポートし、40 単位の注文と 3 キログラムの荷物に対して関数を呼び出します。ただし、アサーションが 1 つ未完成のまま残されています。ファイルを開きます。

nano tests/order_workflow.rs

// INTEGRATION_ASSERTION で終わるプレースホルダーのアサーション行を、次のコードに置き換えます。

    assert_eq!(total, 45);

期待される合計値は、40 単位の小計に、軽量荷物の配送費 5 単位を加えたものです。Nano で保存して終了します。

--test order_workflow オプションは、tests/order_workflow.rs に対応する統合テストターゲットを選択します。

cargo test --test order_workflow

このターゲットの公開ワークフローテストは、次のように表示されます。

test order_total_includes_shipping ... ok

test result: ok. 1 passed; 0 failed; ...

最後に、フィルターを指定せず、パッケージ全体のテストスイートを実行します。

cargo test

Cargo は 3 つのユニットテストと統合テストを実行します。これらは異なるテストバイナリであるため、結果の概要が別々に表示されるのは正常です。すべてのテスト名に ok が表示され、すべての概要で失敗数が 0 になっていることを確認してください。

まとめ

ローカルのユニットテストを追加し、ルールの境界をカバーし、ライブラリの公開 API を通した統合テストを完成させました。また、Cargo のフィルターを使って、1 つのテスト、1 つのモジュール、1 つの統合テストターゲット、またはテストスイート全体を実行しました。これらの習慣により、開発中は迅速に動作を確認でき、引き継ぎ前にはより広範囲のリグレッションを防止できます。