Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

テストの構成

この章の冒頭で述べたように、テストは複雑な分野であり、人によって用語や構成の仕方が異なります。Rust コミュニティでは、テストを主に 2 つのカテゴリ、すなわち単体テストと結合テストとして捉えます。単体テスト は小さく、より焦点が絞られており、一度に 1 つのモジュールを他から切り離してテストし、非公開インターフェースもテストできます。結合テスト はライブラリの完全に外部にあり、他の外部コードが行うのと同じ方法であなたのコードを使います。つまり、公開インターフェースだけを使い、1 つのテストで複数のモジュールを扱うこともあります。

ライブラリを構成する各部分が、個別にも組み合わせても期待どおりに動作していることを確認するには、両方の種類のテストを書くことが重要です。

単体テスト

単体テストの目的は、コードの各単位を残りのコードから切り離してテストし、コードのどこが期待どおりに動作し、どこが動作していないのかをすばやく特定することです。単体テストは、テスト対象のコードがある各ファイル内の src ディレクトリに置きます。慣例では、各ファイルにテスト関数を含む tests という名前のモジュールを作成し、そのモジュールに cfg(test) を付けます。

tests モジュールと #[cfg(test)]

tests モジュールに付ける #[cfg(test)] アノテーションは、Rust に対して、cargo test を実行したときだけテストコードをコンパイルして実行し、cargo build を実行したときにはそうしないよう伝えます。これにより、ライブラリだけをビルドしたいときのコンパイル時間を節約でき、テストが含まれないため、生成されるコンパイル成果物の容量も節約できます。結合テストは別のディレクトリに置かれるので、#[cfg(test)] アノテーションは必要ありません。しかし、単体テストはコードと同じファイルに置かれるため、コンパイル結果に含めるべきでないことを指定するために #[cfg(test)] を使います。

この章の最初の節で新しい adder プロジェクトを生成したとき、Cargo が次のコードを生成してくれたことを思い出してください。

ファイル名: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

自動生成された tests モジュールでは、属性 cfg設定 を表し、特定の設定オプションが与えられた場合にのみ後続の項目を含めるよう Rust に伝えます。この場合の設定オプションは test で、Rust がテストをコンパイルして実行するために提供しているものです。cfg 属性を使うことで、Cargo は cargo test で明示的にテストを実行した場合にのみテストコードをコンパイルします。これには、#[test] が付けられた関数に加えて、このモジュール内にある補助関数も含まれます。

非公開関数のテスト

テストコミュニティの中では、非公開関数を直接テストすべきかどうかについて議論があります。また、他の言語では非公開関数のテストが難しかったり不可能だったりすることもあります。どのようなテストの考え方に従うにせよ、Rust の可視性ルールでは非公開関数をテストできます。リスト 11-12 の、非公開関数 internal_adder を含むコードを見てみましょう。

pub fn add_two(a: u64) -> u64 {
    internal_adder(a, 2)
}

fn internal_adder(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}

internal_adder 関数には pub が付いていないことに注目してください。テストも単なる Rust コードであり、tests モジュールも単なる別のモジュールです。「モジュールツリー内のアイテムを参照するためのパス」 で説明したように、子モジュール内のアイテムは、その祖先モジュール内のアイテムを使えます。このテストでは、use super::* を使って tests モジュールの親に属するすべてのアイテムをスコープに取り込み、そのうえでテストから internal_adder を呼び出せます。非公開関数をテストすべきではないと考えるなら、Rust にそれを強制するものは何もありません。

結合テスト

Rust では、結合テストはライブラリの完全に外部にあります。それらは他のコードと同じ方法であなたのライブラリを使うため、呼び出せるのはライブラリの公開 API の一部である関数だけです。その目的は、ライブラリの多くの部分が正しく連携して動作するかどうかをテストすることです。それぞれ単独では正しく動作するコードの単位でも、統合すると問題が起こることがあります。そのため、統合されたコードに対するテストカバレッジも重要です。結合テストを作成するには、まず tests ディレクトリが必要です。

tests ディレクトリ

プロジェクトディレクトリの最上位で、src の隣に tests ディレクトリを作成します。Cargo はこのディレクトリ内で結合テストファイルを探すようになっています。その後は必要なだけテストファイルを作成でき、Cargo はそれぞれのファイルを個別のクレートとしてコンパイルします。

結合テストを作成してみましょう。引き続き src/lib.rs ファイルにあるリスト 11-12 のコードを使って、tests ディレクトリを作成し、tests/integration_test.rs という新しいファイルを作成します。ディレクトリ構造は次のようになります。

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

tests/integration_test.rs ファイルにリスト 11-13 のコードを入力してください。

use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}

tests ディレクトリ内の各ファイルは別々のクレートなので、各テストクレートのスコープに自分たちのライブラリを取り込む必要があります。そのため、単体テストでは不要だった use adder::add_two; をコードの先頭に追加します。

tests/integration_test.rs 内のコードに #[cfg(test)] を付ける必要はありません。Cargo は tests ディレクトリを特別扱いし、このディレクトリ内のファイルは cargo test を実行したときにだけコンパイルします。では、ここで cargo test を実行しましょう。

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

出力は 3 つのセクションに分かれており、単体テスト、結合テスト、ドキュメントテストが含まれています。あるセクション内のテストが 1 つでも失敗すると、それ以降のセクションは実行されない点に注意してください。たとえば、単体テストが失敗すると、結合テストやドキュメントテストの出力は表示されません。これらのテストは、すべての単体テストが成功した場合にのみ実行されるからです。

単体テストの最初のセクションは、これまで見てきたものと同じです。各単体テストごとに 1 行(リスト 11-12 で追加した internal という名前のものが 1 つあります)があり、その後に単体テストの要約行が続きます。

結合テストのセクションは、Running tests/integration_test.rs という行で始まります。次に、その結合テスト内の各テスト関数について 1 行ずつ表示され、Doc-tests adder セクションが始まる直前に、結合テストの結果の要約行が表示されます。

各結合テストファイルにはそれぞれ専用のセクションがあるので、tests ディレクトリにさらにファイルを追加すれば、結合テストのセクションも増えます。 特定の統合テスト関数は、cargo test の引数としてテスト関数名を指定することで、引き続き実行できます。特定の統合テストファイル内のすべてのテストを実行するには、cargo test--test 引数の後にファイル名を指定します。

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

このコマンドは、tests/integration_test.rs ファイル内のテストだけを実行します。

統合テスト内のサブモジュール

統合テストをさらに追加していくと、それらを整理しやすくするために tests ディレクトリ内にさらにファイルを作りたくなるかもしれません。たとえば、テスト関数を、テスト対象の機能ごとにグループ化できます。前に述べたように、tests ディレクトリ内の各ファイルは、それぞれ独立した別個のクレートとしてコンパイルされます。これは、個別のスコープを作成して、エンドユーザーがあなたのクレートを使う方法をより厳密に模倣するうえで便利です。しかしこれは、tests ディレクトリ内のファイルが、src 内のファイルと同じ振る舞いを共有しないことも意味します。これは、コードをモジュールとファイルに分割する方法について第7章で学んだとおりです。

tests ディレクトリ内のファイルのこの違いは、複数の統合テストファイルで使うヘルパー関数群があり、それらを共通モジュールに抽出するために、第7章の「モジュールを別々のファイルに分割する」節の手順に従おうとしたときに、もっとも顕著に現れます。たとえば、tests/common.rs を作成して、その中に setup という名前の関数を置いた場合、複数のテストファイル内の複数のテスト関数から呼び出したいコードを setup に追加できます。

ファイル名: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

再びテストを実行すると、common.rs ファイルにはテスト関数がまったく含まれておらず、また setup 関数をどこからも呼び出していないにもかかわらず、テスト出力に common.rs ファイル用の新しいセクションが表示されます。

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

テスト結果に common が現れ、その項目に running 0 tests と表示されるのは、私たちが望んでいたことではありません。私たちは単に、ほかの統合テストファイルとコードを共有したかっただけです。common がテスト出力に現れないようにするため、tests/common.rs を作成する代わりに、tests/common/mod.rs を作成します。これでプロジェクトディレクトリは次のようになります。

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

これは、Rust が理解する古い命名規約であり、第7章の「別のファイルパス」で触れたものです。このようにファイルに名前を付けることで、common モジュールを統合テストファイルとして扱わないよう Rust に伝えられます。setup 関数のコードを tests/common/mod.rs に移し、tests/common.rs ファイルを削除すると、そのセクションはテスト出力に表示されなくなります。tests ディレクトリのサブディレクトリ内にあるファイルは、独立したクレートとしてコンパイルされず、テスト出力内にセクションも作られません。

tests/common/mod.rs を作成した後は、どの統合テストファイルからでもそれをモジュールとして使えます。次は、tests/integration_test.rsit_adds_two テストから setup 関数を呼び出す例です。

ファイル名: tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

mod common; 宣言は、リスト7-21で示したモジュール宣言と同じであることに注意してください。そしてテスト関数内では、common::setup() 関数を呼び出せます。

バイナリクレートの統合テスト

プロジェクトが src/main.rs ファイルしか含まず、src/lib.rs ファイルを持たないバイナリクレートである場合、tests ディレクトリに統合テストを作成して、src/main.rs ファイルで定義された関数を use 文でスコープに持ち込むことはできません。他のクレートが利用できる関数を公開するのはライブラリクレートだけであり、バイナリクレートは単独で実行されることを意図しているためです。

これは、バイナリを提供する Rust プロジェクトが、src/lib.rs ファイル内にあるロジックを呼び出す、単純な src/main.rs ファイルを持つ構造になっている理由の1つです。その構造を使えば、統合テストは use によってライブラリクレートをテストし、重要な機能を利用可能にできます。重要な機能が正しく動作すれば、src/main.rs ファイル内の少量のコードも同様に動作します。そして、その少量のコードはテストする必要がありません。

まとめ

Rust のテスト機能は、コードがどのように動作すべきかを指定する手段を提供し、変更を加えても期待どおりに動作し続けることを保証できるようにします。ユニットテストはライブラリのさまざまな部分を個別に検証し、非公開の実装詳細もテストできます。統合テストはライブラリの多くの部分が正しく連携して動作することを確認し、外部コードが利用するのと同じ方法でライブラリの公開 API を使ってコードをテストします。Rust の型システムと所有権ルールは、ある種のバグを防ぐのに役立ちますが、それでも、コードがどのように振る舞うべきかに関わるロジックバグを減らすためにテストは重要です。

この章とこれまでの章で学んだ知識を組み合わせて、プロジェクトに取り組んでみましょう。