はじめに
テスト駆動開発によるライブラリ機能の開発へようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。
この実験では、テスト駆動開発を使ってライブラリの機能を開発し、プログラムに検索ロジックを追加します。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
テスト駆動開発によるライブラリ機能の開発へようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。
この実験では、テスト駆動開発を使ってライブラリの機能を開発し、プログラムに検索ロジックを追加します。
これで、論理をsrc/lib.rs
に抽出し、引数の収集とエラー処理をsrc/main.rs
に残したので、コードのコア機能のテストを書くのがはるかに簡単になりました。コマンドラインからバイナリを呼び出す必要なく、さまざまな引数で関数を直接呼び出し、戻り値をチェックすることができます。
このセクションでは、テスト駆動開発(TDD)プロセスを使って、minigrep
プログラムに検索ロジックを追加します。以下の手順で行います。
TDDはソフトウェアを書くための多くの方法の1つにすぎませんが、コードの設計を促進する役に立ちます。テストを通過させるコードを書く前にテストを書くことは、プロセス全体を通じて高いテストカバレッジを維持するのに役立ちます。
ファイルの内容からクエリ文字列を検索し、一致する行のリストを生成する機能の実装をテスト駆動で行います。この機能をsearch
と呼ばれる関数に追加します。
もはや必要ないので、src/lib.rs
とsrc/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
を実装するには、プログラムは次の手順に従う必要があります。
各行を反復処理することから始めて、それぞれの手順を見ていきましょう。
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章でこの例に戻り、イテレータを詳細に検討し、それをどのように改善するかを見ていきます。
search
関数が機能し、テストに合格したので、run
関数からsearch
を呼び出す必要があります。config.query
の値と、run
がファイルから読み取ったcontents
をsearch
関数に渡す必要があります。そして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でさらに多くの実験を行ってみてください。