いよいよ「テスト駆動開発」に入っていこう。
まずは、P4の最初のテストだ。おもむろに存在しないDollerクラスをnewして、 ありもしないtimesメソッドやamountフィールドを参照している。テストファーストなのだ。
・・・Rustでデータ型を表現するのはどうするんだったかな・・・(本をめくる)・・・ 構造体だ。ざっと眺めて要点を飲み込む。
こんな感じでいいんだろうか。
fn main() { println!("Hello, world!"); } #[cfg(test)] mod tests { use super::*; #[test] fn test_multiplication() { let five = Doller { amount: 5 }; five.times(2); assert_eq!(10, five.amount); } }
まず、Rustにおいて関数のテストは同じファイル内に書く。
単に#[test]
と属性を付ければ、それがテストだと見なされる。
ただし、慣例としてtestsというモジュールを作り、その中に入れる。
そこに#[cfg(test)]
と属性を付けておけば、テストはtest実行以外の場合ではビルド対象から外される。
で、VScodeでテストを走らせるにはどうしたら良いのか。
コードで#[test]
と書くと、その下に Run test という表示が出て、押すとテストが走る。
これはRust(rls)拡張機能がやっているらしい。
押すと、ターミナルが開き、こんな感じのログが流れた。
> Executing task: cargo test -- --nocapture test_multiplication < Compiling money v0.1.0 (/Users/tambara/study/tdd_rust/money) error[E0422]: cannot find struct, variant or union type `Doller` in this scope --> src/main.rs:11:20 | 11 | let five = Doller { amount: 5 }; | ^^^^^^ not found in this scope warning: unused import: `super::*` --> src/main.rs:7:9 | 7 | use super::*; | ^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default error: aborting due to previous error For more information about this error, try `rustc --explain E0422`. error: could not compile `money`. To learn more, run the command again with --verbose. The terminal process terminated with exit code: 101 Terminal will be reused by tasks, press any key to close it.
要するに cargo で特定のテストだけ実行しているわけだ。
テストを全部流したかったらどうするのか・・・メニューのTerminal - Run Taskから出来るみたい。
> Executing task: cargo test < Compiling money v0.1.0 (/Users/tambara/study/tdd_rust/money) error[E0422]: cannot find struct, variant or union type `Doller` in this scope --> src/main.rs:11:20 | 11 | let five = Doller { amount: 5 }; | ^^^^^^ not found in this scope warning: unused import: `super::*` --> src/main.rs:7:9 | 7 | use super::*; | ^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default error: aborting due to previous error For more information about this error, try `rustc --explain E0422`. error: could not compile `money`. To learn more, run the command again with --verbose. The terminal process terminated with exit code: 101 Terminal will be reused by tasks, press any key to close it.
確かにcargo testをやってる。
で、エラーの方は、Dollerってなんやねんと言われている。それはそうだ。
P6 でやっているように、空の実装を足してやる。
// これ消すと怒られるのなぜ? fn main() {} pub struct Doller { pub amount: i32, } impl Doller { pub fn times(multiplier: i32) {} } #[cfg(test)] mod tests { use super::*; #[test] fn test_multiplication() { let five = Doller { amount: 5 }; five.times(2); assert_eq!(10, five.amount); } }
良さそうに思えるけど、まだコンパイルが通らない。
error[E0599]: no method named `times` found for struct `Doller` in the current scope --> src/main.rs:19:14 | 4 | pub struct Doller { | ----------------- method `times` not found for this ... 19 | five.times(2); | -----^^^^^ | | | | | this is an associated function, not a method | help: use associated function syntax instead: `Doller::times` | = note: found the following associated functions; to be used as methods, functions must have a `self` parameter note: the candidate is defined in an impl for the type `Doller` --> src/main.rs:9:5 | 9 | pub fn times(multiplier: i32) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Rustはコンパイルを通すのが凄く難しい言語だが、 エラーメッセージが驚くほど親切だ。
Dollerにはtimesなんてメソッドはないぞと怒られているんだけども、 「お前、selfをパラメータに入れ忘れてるんじゃね?」と教えてくれている。
そう、Rustで構造体のメソッドを定義したいのなら、1つ目のメソッドはselfにしないといけない。 こうだ。
impl Doller { pub fn times(&mut self, multiplier: i32) {} }
エラーが変わった。
--> src/main.rs:19:9 | 18 | let five = Doller { amount: 5 }; | ---- help: consider changing this to be mutable: `mut five` 19 | five.times(2); | ^^^^ cannot borrow as mutable
fiveがmutableじゃないとダメと怒られた。それはそうだ。
fn test_multiplication() { let mut five = Doller { amount: 5 }; five.times(2); assert_eq!(10, five.amount); }
こうなってないといけない。これでエラーが消えた。
> Executing task: cargo test < warning: unused variable: `multiplier` --> src/main.rs:9:29 | 9 | pub fn times(&mut self, multiplier: i32) {} | ^^^^^^^^^^ help: consider prefixing with an underscore: `_multiplier` | = note: `#[warn(unused_variables)]` on by default Finished test [unoptimized + debuginfo] target(s) in 0.00s Running target/debug/deps/money-043e3ba9eebcce93 running 1 test test tests::test_multiplication ... FAILED failures: ---- tests::test_multiplication stdout ---- thread 'tests::test_multiplication' panicked at 'assertion failed: `(left == right)` left: `10`, right: `5`', src/main.rs:20:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::test_multiplication test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--bin money' The terminal process terminated with exit code: 101
うん。ちゃんとテスト結果も出ている。FAILEDだ。 しかし、グリーンなのかレッドなのかわかりにくいな・・・。
では、グリーンにしておこうか。
// これ消すと怒られるのなぜ? fn main() {} pub struct Doller { pub amount: i32, } impl Doller { pub fn times(&mut self, multiplier: i32) { self.amount *= multiplier; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_multiplication() { let mut five = Doller { amount: 5 }; five.times(2); assert_eq!(10, five.amount); } }
> Executing task: cargo test -- --nocapture test_multiplication < Finished test [unoptimized + debuginfo] target(s) in 0.00s Running target/debug/deps/money-043e3ba9eebcce93 running 1 test test tests::test_multiplication ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
テストが通った。素晴らしい。これで、1章は終わりだ。