TDD
AdventCalendar
Rust
unittest
Rustlang
RustDay 5

書籍「テスト駆動開発」をRustで書く #1 (chapter1...3)

More than 1 year has passed since last update.

はじめに

これはRustその2 Advent Calendar 2017の9日目の記事です。

@Khigashiguchiです。
著書「テスト駆動開発」をrustで書いていきます。
テスト駆動開発」は、Kent Beck著、和田 卓人さん翻訳の書籍です。この著書では、第1章から第17章にかけて、「多国通貨」というテーマでテスト駆動開発のプロセスを写経しながら体感することができる構成になっています。
著作はJavaで進められていますが、今回はそれをrustで進めていきます。
最終的に目指すコードは以下のテストが通るコードです。

  • 通貨の異なる2つの金額を足し、通貨間の為替レートに基づいて換算された金額を得る。
  • 金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る

目次

  • 概要
  • 第1章 仮実装
  • 第2章 明白な実装
  • 第3章 三角測量
  • 最後に

概要

本ページでは、第1章から第4章までを進めていきます。第5章以降は、別記事にて更新していきます。

基本的な流れとして、以下のプロセスを進めていきます
- 今時点のToDoList
- テストを書く
- テストを通す最低限の実装をする
- リファクタリングを行う
- 最終的なコード

Khigashiguchi/rust_sutras_tddに今回のコードを残しました。
(まだまだ、不慣れなところがあるのでこうしたほうがいいという意見があったら是非いただきたいです。)

rustのユニットテスト自体の書き方については、RustでUnit Testを書く方法に簡単に書かせていただいています。

第1章 仮実装

「多国通貨」の実装にあたり、やらなければいけない実装をToDo Listに残します。

今時点のToDoList

  • $5 + 10CHF = $10 (レートが2:1の場合)
  • $5 * 2 = $10

テストを書く

いきなりToDoListのテストを通すのは実装が大きくなりそうなので、小さいテストを書く。
まずは、同一通貨での掛け算をテストとして書く。

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let five = Dollar::new(5);
        five.times(2);
        assert_eq!(10, five.amount);
    }
}

実行してビルドが失敗するところを見届けます。

$ cargo test
   Compiling sutras_tdd v0.1.0 (file:///Users/kazukihigashiguchi/src/hobby/rust/sutras_tdd)
error[E0433]: failed to resolve. Use of undeclared type or module `Dollar`
 --> src/lib.rs:6:20
  |
6 |         let five = Dollar::new(5);
  |                    ^^^^^^ Use of undeclared type or module `Dollar`

error: aborting due to previous error

error: Could not compile `sutras_tdd`.
warning: build failed, waiting for other jobs to finish...
error: build failed

テストを書いた結果、いくつか問題点が出てきたのでToDoListに書き残します。

ToDoList

  • $5 + 10CHF = $10 (レートが2:1の場合)
  • $5 * 2 = $10
  • amountをprivateにする 追加
  • Dollarの副作用を解消する 追加
  • Moneyの丸め処理をする 追加

テストを通す最低限の実装をする

まずはテストが通るまで仮実装します。

#[derive(Debug)]
pub struct Dollar {
    amount: u32
}

impl Dollar {
    pub fn new(amount: u32) -> Dollar {
        Dollar { amount: 10 }
    }
    pub fn times (&self, multiplier: u32) {
    }
}

unusedのwarningは出ていますが、テストは通りました。でも、ひどいコードです。
これからリファクタリングしていきます。

リファクタリングを行う

以下、修正していった内容は差分のcommitに残します。

$ cargo test

running 1 test
test tests::test_multiplication ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

ここからベタがきだったりする部分をリファクタリングしていきます。修正するごとにテストを実行して成功することを確認していきます。

  • ベタがきの10は、頭の中での掛け算の結果だったので、5*2に分解。
-        Dollar { amount: 10 }
+        Dollar { amount: 5 * 2 }

差分commit

  • 5*2の5はnew()の引数amountを利用する
-       Dollar { amount: 5 * 2 }
+       Dollar { amount: amount }

-    pub fn times (&self, multiplier: u32) {
+    pub fn times (&mut self, multiplier: u32) {
+        self.amount = self.amount * 2;

-        let five = Dollar::new(5);
+        let mut five = Dollar::new(5);

差分commit

  • 5*2の2はtimes()の引数multiplierを利用する
-        self.amount = self.amount * 2;
+        self.amount = self.amount * multiplier;

差分commit

最終的なコード

第1章の最終的なコードは以下のようになりました。

#[derive(Debug)]
pub struct Dollar {
    amount: u32
}

impl Dollar {
    pub fn new(amount: u32) -> Dollar {
        Dollar { amount: amount }
    }
    pub fn times (&mut self, multiplier: u32) {
        self.amount = self.amount * multiplier;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let mut five = Dollar::new(5);
        five.times(2);
        assert_eq!(10, five.amount);
    }
}

テスト実行結果

$ cargo test

running 1 test
test tests::test_multiplication ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

以下に最終的なコードを残しています。
https://github.com/Khigashiguchi/rust_sutras_tdd/blob/chapter/1/src/lib.rs

第2章 明白な実装

第二章では今時点のToDoList内の、「Dollarの副作用を解消する」をこなしていきます。
現在の実装では、Dollarのamountがmutableであるため、状態が操作のたびに変化してしまいます。
期待値をテストとして定義して実装を進めていきます。

今時点のToDoList

  • $5 + 10CHF = $10 (レートが2:1の場合)
  • $5 * 2 = $10
  • amountをprivateにする
  • Dollarの副作用を解消する
  • Moneyの丸め処理をする

テストを書く

まずは、こんな感じで使いたいという関数に対する期待値を書いてみます。

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let mut five = Dollar::new(5);
        five.times(2);
        assert_eq!(10, five.amount);
        five.times(3); // 追記
        assert_eq!(15, five.amount); // 追記
    }
}

このテストを実行すると以下の結果になります。

-> % cargo test

running 1 test
test tests::test_multiplication ... FAILED

failures:

---- tests::test_multiplication stdout ----
    thread 'tests::test_multiplication' panicked at 'assertion failed: `(left == right)`
  left: `15`,
 right: `30`', src/lib.rs:24:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.


failures:
    tests::test_multiplication

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

テストが落ちることは確認できたので、通るように実装を考えていきたいです。
今回は、times()の返り値で新しいオブジェクトを返せば、元の5ドルは変わらずテストを通すことができそうです。
テストを通すにあたり、DollerのI/Fも変わるのでテストコードを修正します。

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let five = Dollar::new(5);
        let mut product = five.times(2);
        assert_eq!(10, product.amount);
        product = five.times(3);
        assert_eq!(15, product.amount);
    }
}

テストを通す最低限の実装をする

以降、リファクタリングの行程は省略させていただきますが、Khigashiguchi/rust_sutras_tddのコミット履歴にリファクタリングの過程を残しました。

#[derive(Debug)]
pub struct Dollar {
    amount: u32
}

impl Dollar {
    pub fn new(amount: u32) -> Dollar {
        Dollar { amount: amount }
    }
    pub fn times (&self, multiplier: u32) -> Dollar {
        Dollar { amount: self.amount * multiplier }
    }
}

前回のコードとの差分は、Khigashiguchi/rust_sutras_tdd/pull/2を参照ください。

最終的なコード

#[derive(Debug)]
pub struct Dollar {
    amount: u32
}

impl Dollar {
    pub fn new(amount: u32) -> Dollar {
        Dollar { amount: amount }
    }
    pub fn times (&self, multiplier: u32) -> Dollar {
        Dollar { amount: self.amount * multiplier }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let five = Dollar::new(5);
        let mut product = five.times(2);
        assert_eq!(10, product.amount);
        product = five.times(3);
        assert_eq!(15, product.amount);
    }
}

以下に、最終的なコードを残しています。
https://github.com/Khigashiguchi/rust_sutras_tdd/blob/chapter/2/src/lib.rs

第3章 三角測量

今時点のToDoList

  • $5 + 10CHF = $10 (レートが2:1の場合)
  • $5 * 2 = $10
  • amountをprivateにする
  • Dollarの副作用を解消する
  • Moneyの丸め処理をする

テストを書く

第2章の実装では、別名参照を気にする必要がないメリットのあるValue Objectパターンを採用した。オブジェクトがValue Objectであるためには操作は全て新しいオブジェクトを返す必要がある。
また、Value Objectはequals()を実装する必要がある。
また、Dollarオブジェクトをハッシュテーブルのキーとして使うのであれば、hashCode()も必要になる。
ToDoListを追加する。

ToDoList

  • $5 + 10CHF = $10 (レートが2:1の場合)
  • $5 * 2 = $10
  • amountをprivateにする
  • Dollarの副作用を解消する
  • Moneyの丸め処理をする
  • equal()
  • hashCode()

テストを書く

    #[test]
    fn test_equality() {
        assert!(Dollar::new(5).equals(Dollar::new(5)));
        assert!(!Dollar::new(5).equals(Dollar::new(6)));
    }

テストを通す最低限の実装をする

テストを実行すると、equals()が無いと怒られるのでequals()を実装する。

$ cargo test
   Compiling sutras_tdd v0.1.0 
error[E0599]: no method named `equals` found for type `Dollar` in the current scope
  --> src/lib.rs:28:32
   |
28 |         assert!(Dollar::new(5).equals(Dollar::new(5)));
   |                                ^^^^^^

error: aborting due to previous error

error: Could not compile `sutras_tdd`.
warning: build failed, waiting for other jobs to finish...
error: build failed

以下、実装

    pub fn equals (&self, target: Dollar) -> bool {
        self.amount == target.amount
    }

最終的なコード

#[derive(Debug)]
pub struct Dollar {
    amount: u32
}

impl Dollar {
    pub fn new(amount: u32) -> Dollar {
        Dollar { amount: amount }
    }
    pub fn times (&self, multiplier: u32) -> Dollar {
        Dollar { amount: self.amount * multiplier }
    }
    pub fn equals (&self, target: Dollar) -> bool {
        self.amount == target.amount
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let five = Dollar::new(5);
        let mut product = five.times(2);
        assert_eq!(10, product.amount);
        product = five.times(3);
        assert_eq!(15, product.amount);
    }
    #[test]
    fn test_equality() {
        assert!(Dollar::new(5).equals(Dollar::new(5)));
        assert!(!Dollar::new(5).equals(Dollar::new(6)));
    }
}

以下に、最終的なコードを残しています。
https://github.com/Khigashiguchi/rust_sutras_tdd/blob/chapter/3/src/lib.rs

最後に

ここはこうしたほうがいいというコメントがありましたら、是非いただきたいです!
次回、第4章以降を進めていきます。

 参考