Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(3)

いよいよ「テスト駆動開発」に入っていこう。

まずは、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章は終わりだ。