テスト駆動開発による Rust ライブラリ機能

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

テスト駆動開発によるライブラリ機能の開発へようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。

この実験では、テスト駆動開発を使ってライブラリの機能を開発し、プログラムに検索ロジックを追加します。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100421{{"テスト駆動開発による Rust ライブラリ機能"}} rust/mutable_variables -.-> lab-100421{{"テスト駆動開発による Rust ライブラリ機能"}} rust/for_loop -.-> lab-100421{{"テスト駆動開発による Rust ライブラリ機能"}} rust/function_syntax -.-> lab-100421{{"テスト駆動開発による Rust ライブラリ機能"}} rust/expressions_statements -.-> lab-100421{{"テスト駆動開発による Rust ライブラリ機能"}} rust/method_syntax -.-> lab-100421{{"テスト駆動開発による Rust ライブラリ機能"}} rust/operator_overloading -.-> lab-100421{{"テスト駆動開発による Rust ライブラリ機能"}} end

テスト駆動開発

これで、論理をsrc/lib.rsに抽出し、引数の収集とエラー処理をsrc/main.rsに残したので、コードのコア機能のテストを書くのがはるかに簡単になりました。コマンドラインからバイナリを呼び出す必要なく、さまざまな引数で関数を直接呼び出し、戻り値をチェックすることができます。

このセクションでは、テスト駆動開発(TDD)プロセスを使って、minigrepプログラムに検索ロジックを追加します。以下の手順で行います。

  1. 失敗するテストを書き、それが期待通りに失敗することを確認するために実行します。
  2. 新しいテストが通過するように、必要なだけのコードを書き加えるか修正します。
  3. 追加または変更したコードをリファクタリングし、テストが引き続き通過することを確認します。
  4. 手順1から繰り返します!

TDDはソフトウェアを書くための多くの方法の1つにすぎませんが、コードの設計を促進する役に立ちます。テストを通過させるコードを書く前にテストを書くことは、プロセス全体を通じて高いテストカバレッジを維持するのに役立ちます。

ファイルの内容からクエリ文字列を検索し、一致する行のリストを生成する機能の実装をテスト駆動で行います。この機能をsearchと呼ばれる関数に追加します。

失敗するテストを書く

もはや必要ないので、src/lib.rssrc/main.rsから、プログラムの動作を確認するために使っていたprintln!文を削除しましょう。そして、src/lib.rsでは、11章でやったように、テスト関数を持つtestsモジュールを追加します。このテスト関数は、search関数が持つべき動作を指定します。つまり、クエリと検索対象のテキストを受け取り、テキストからクエリに一致する行のみを返すでしょう。リスト12-15にこのテストを示しますが、まだコンパイルされません。

ファイル名: src/lib.rs

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(
            vec!["safe, fast, productive."],
            search(query, contents)
        );
    }
}

リスト12-15: 持ちたいsearch関数に対する失敗するテストを作成する

このテストは、文字列"duct"を検索します。検索対象のテキストは3行で、そのうち1行のみが"duct"を含んでいます(最初の二重引用符の後のバックスラッシュは、Rustにこの文字列リテラルの内容の先頭に改行文字を置かないように指示しています)。search関数から返される値が、期待する行のみを含んでいることをアサートします。

このテストを実行して失敗するのを見ることはまだできません。なぜなら、テスト自体がコンパイルされないからです。search関数がまだ存在しないのです!TDDの原則に従って、常に空のベクトルを返すsearch関数の定義を追加することで、テストがコンパイルされて実行されるように、必要なだけのコードを追加します。そうすると、空のベクトルが"safe, fast, productive."の行を含むベクトルと一致しないため、テストはコンパイルされて失敗するはずです。

ファイル名: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    vec![]
}

リスト12-16: テストがコンパイルされるように、search関数を十分に定義する

searchのシグネチャで明示的な寿命期間'aを定義し、その寿命期間をcontents引数と返り値に使用することに注意してください。10章で思い出してください。寿命期間パラメータは、どの引数の寿命期間が返り値の寿命期間と関連付けられているかを指定します。この場合、返されるベクトルが、引数contents(引数queryではない)のスライスを参照する文字列スライスを含むことを示しています。

言い換えると、search関数が返すデータは、contents引数でsearch関数に渡されるデータと同じくらい長く生き続けることをRustに伝えています。これは重要です!スライスによって参照されるデータは、参照が有効であるために有効でなければなりません。コンパイラがqueryの文字列スライスではなくcontentsの文字列スライスを作っていると仮定すると、安全性のチェックが誤って行われます。

寿命期間の注釈を忘れてこの関数をコンパイルしようとすると、このエラーが表示されます。

error[E0106]: missing lifetime specifier
  --> src/lib.rs:31:10
   |
29 |     query: &str,
   |            ----
30 |     contents: &str,
   |               ----
31 | ) -> Vec<&str> {
   |          ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
   |
28 ~ pub fn search<'a>(
29 ~     query: &'a str,
30 ~     contents: &'a str,
31 ~ ) -> Vec<&'a str> {
   |

Rustは、2つの引数のどちらが必要かを知ることができません。だから、明示的に伝える必要があります。contentsがすべてのテキストを含む引数で、一致するテキストの部分を返したいので、contentsが寿命期間構文を使って返り値と関連付けられるべき引数であることがわかります。

他のプログラミング言語では、シグネチャで引数を返り値に関連付ける必要はありませんが、時間が経つにつれてこの方法が慣れるようになります。この例を「寿命期間で参照を検証する」の例と比較してみると良いかもしれません。

さて、テストを実行しましょう。

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 0.97s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result... FAILED

failures:

---- tests::one_result stdout ----
thread 'tests::one_result' panicked at 'assertion failed: `(left == right)`
  left: `["safe, fast, productive."]`,
 right: `[]`', src/lib.rs:47:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::one_result

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

error: test failed, to rerun pass '--lib'

素晴らしい!テストは、予想通りに失敗しました。テストを通過させましょう!

テストを通過させるためのコードを書く

現在、テストが失敗しているのは、常に空のベクトルを返しているからです。それを修正してsearchを実装するには、プログラムは次の手順に従う必要があります。

  1. コンテンツの各行を反復処理する。
  2. その行がクエリ文字列含んでいるかどうかを確認する。
  3. 含んでいる場合、返す値のリストに追加する。
  4. 含んでいない場合、何もしない。
  5. 一致する結果のリストを返す。

各行を反復処理することから始めて、それぞれの手順を見ていきましょう。

linesメソッドを使って行を反復処理する

Rustには、文字列の行ごとの反復処理を行うのに便利なメソッドがあり、便利にもlinesという名前が付けられています。これはリスト12-17に示すように機能します。ただし、これはまだコンパイルされません。

ファイル名: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

リスト12-17: contentsの各行を反復処理する

linesメソッドはイテレータを返します。13章でイテレータについて詳しく説明しますが、3-5のリストでイテレータを使ったこの方法を見たことを思い出してください。そこでは、コレクションの各要素に対して何かコードを実行するために、イテレータを使ったforループを使っていました。

各行をクエリで検索する

次に、現在の行がクエリ文字列を含んでいるかどうかを確認します。幸いなことに、文字列にはこれを行うための便利なcontainsメソッドがあります!search関数にcontainsメソッドの呼び出しを追加します。これはリスト12-18に示すようになります。ただし、これはまだコンパイルされません。

ファイル名: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

リスト12-18: 行がqueryの文字列含んでいるかどうかを確認する機能を追加する

今のところ、機能を追加しています。コードをコンパイルするには、関数シグネチャで示した通り、本体から値を返す必要があります。

一致する行を格納する

この関数を完成させるには、返す一致する行を格納する方法が必要です。そのために、forループの前に可変ベクトルを作成し、pushメソッドを呼び出してベクトルにlineを格納します。forループの後で、ベクトルを返します。これはリスト12-19に示すようになります。

ファイル名: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

リスト12-19: 一致する行を格納して返すためにする

これでsearch関数はqueryを含む行のみを返すようになり、テストも通過するはずです。テストを実行しましょう。

$ cargo test
--snip--
running 1 test
test tests::one_result... ok

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

テストが通過しましたので、機能することがわかります!

この時点で、テストを通過させたままで同じ機能を維持しながら、検索関数の実装をリファクタリングする機会を検討することができます。検索関数のコードはあまり悪くはありませんが、イテレータのいくつかの便利な機能を利用していません。13章でこの例に戻り、イテレータを詳細に検討し、それをどのように改善するかを見ていきます。

run関数でsearch関数を使用する

search関数が機能し、テストに合格したので、run関数からsearchを呼び出す必要があります。config.queryの値と、runがファイルから読み取ったcontentssearch関数に渡す必要があります。そしてrunは、searchから返された各行を出力します。

ファイル名: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

まだforループを使って、searchから返された各行を取得して出力しています。

これで、全体のプログラムが機能するはずです!試してみましょう。まずは、エミリー・ディキンソンの詩から正確に1行を返す単語「frog」を使ってみます。

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

素敵!次に、「body」のように複数の行と一致する単語を試してみましょう。

$ cargo run -- body poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

最後に、詩のどこにもない単語「monomorphization」を検索したときに、何も行が返されないことを確認しましょう。

$ cargo run -- monomorphization poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

素晴らしい!私たちは古典的なツールのミニバージョンを自作し、アプリケーションの構造についてたくさん学びました。また、ファイルの入出力、ライフタイム、テスト、コマンドライン解析についても少し学びました。

このプロジェクトを締めくくるために、環境変数の使い方と標準エラーへの出力方法を簡単に示します。これらはコマンドラインプログラムを書く際に便利です。

まとめ

おめでとうございます!テスト駆動開発によるライブラリ機能の開発の実験を完了しました。さらにスキルを向上させるために、LabExでさらに多くの実験を行ってみてください。