LoginSignup
7
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-12-09

はじめに

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

@Khigashiguchiです。
著書「テスト駆動開発」をrustで書いていきます。
テスト駆動開発」は、Kent Beck著、和田 卓人さん翻訳の書籍です。この著書では、第1章から第17章にかけて、「多国通貨」というテーマでテスト駆動開発のプロセスを写経しながら体感することができる構成になっています。

本記事は、以下記事の続編になっています。

目次

  • 概要
  • 前回のおさらい
  • 第7章 疑念をテストに翻訳する
  • 第8章 実装を隠す
  • 第9章 歩幅の調整
  • 第10章 テストに聞いてみる
  • 第11章 不要になったら消す
  • 最後に

概要

本ページでは、第7章から第11章までを進めていきます。第12章以降は、続編にて更新していきます。

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

前回のおさらい

前回時点のToDoList

前回終了時点で、実装タスクは以下のステータスになっています。

  • \$5 + 10CHF = \$10
  • \$5 * 2 = \$10
  • amountをprivateにする(すでにprivateだった)
  • Dollarの副作用どうする
  • Moneyの丸め処理どうする
  • equals()の実装
  • hashCode()の実装
  • nullとの等価性比較
  • 他のオブジェクトとの等価性比較
  • 5CHF * 2 = 10CHF
  • DollarとFrancの重複
  • equalsの一般化
  • timesの一般化

前回時点でのコード

第7章 疑念をテストに翻訳する

「これって大丈夫なんだっけ?」といった疑念はテストコードとして翻訳する。今回、5Fraと5Dollerを比較した時の結果についてテストコードを追加します。

+        assert!(!Franc::new(5).equals(Dollar::new(5)));

差分Commit

このテストを実行すると失敗してくれます。現在のequalsメソッドはamountのみを比較して通貨や為替レートなどの考慮はされていないためです。
原著では、ここからクラスの比較を行う実装をしてテストを通しているのですが、equals()ではMoney同士の比較をしているのでその方法は使えませんでした。(正確には、後でやる実装をrustのコンパイラを通すために先取りする必要があったためです。)
本章ではテストが通っていませんが、次章以降でテストを通すことをToDoListに書き残して次の章に進みます。

最終的なコード

本章終了時点のコードは以下です。
https://github.com/Khigashiguchi/rust_sutras_tdd/blob/chapter/7/src/lib.rs

今時点のToDoList

  • \$5 + 10CHF = \$10
  • \$5 * 2 = \$10
  • amountをprivateにする(すでにprivateだった)
  • Dollarの副作用どうする
  • Moneyの丸め処理どうする
  • equals()の実装
  • hashCode()の実装
  • nullとの等価性比較
  • 他のオブジェクトとの等価性比較
  • 5CHF * 2 = 10CHF
  • DollarとFrancの重複
  • equalsの一般化
  • timesの一般化
  • FrancとDollarを比較する 追加

第8章 実装を隠す

今の実装では、Dollar::new()・Franc::new()で、Moneyを返す仕様ですが、逆にいうとDollar・Francはそれしかしていません。
いっそのこと、これらもMoneyに引き上げられないかというのが本章の視点です。

テストを修正する

「どうやって使いたいか」をテストコードに定義します。

     fn test_multiplication() {
 -        let five = Dollar::new(5);
 -        assert!(Dollar::new(10).equals(five.times(2)));
 -        assert!(Dollar::new(15).equals(five.times(3)));
 +        let five = Money::dollar(5);
 +        assert!(Money::dollar(10).equals(five.times(2)));
 +        assert!(Money::dollar(15).equals(five.times(3)));

上記コードで、Dollar::new() は、Money::dollar()と使いたいと期待値を定義します。
差分Commit
このテストを通すための実装をして行きます。

実装する

impl Money{

+    pub fn dollar(amount: u32) -> Money {
+        Money { amount: amount }
+    }

テストは通ります。Fracも同様に実装して行きます。

+    pub fn franc (amount: u32) -> Money {
+         Money { amount: amount }
+    }

差分Commit

テストは通るようになりました。これでMoneyに一通りの処理が引き上げられ、Dollar・Francは消しても良さそうな状態にまで綺麗になってきました。

最終的なコード

本章終了時点のコードは以下です。

pub struct Money {
    amount: u32
}

trait MoneyTrait {
    fn new(amount: u32) -> Money;
}

pub struct Dollar {
}

pub struct Franc {
}

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

impl MoneyTrait for Dollar {
    fn new (amount: u32) -> Money {
        Money { amount: amount }
    }
}

impl Franc {
    pub fn new (amount: u32) -> Money {
        Money { amount: amount }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let five = Money::dollar(5);
        assert!(Money::dollar(10).equals(five.times(2)));
        assert!(Money::dollar(15).equals(five.times(3)));
    }
    #[test]
    fn test_equality() {
        assert!(Money::dollar(5).equals(Money::dollar(5)));
        assert!(!Money::dollar(5).equals(Money::dollar(6)));
        assert!(Money::franc(5).equals(Money::franc(5)));
        assert!(!Money::franc(5).equals(Money::franc(6)));
        //TODO: assert!(!Money::franc(5).equals(Money::dollar(5)));
    }
    #[test]
    fn test_franc_multiplication() {
        let five = Money::franc(5);
        assert!(Money::franc(10).equals(five.times(2)));
        assert!(Money::franc(15).equals(five.times(3)));
    }
}

今時点でのToDoList

  • \$5 + 10CHF = \$10
  • \$5 * 2 = \$10
  • amountをprivateにする(すでにprivateだった)
  • Dollarの副作用どうする
  • Moneyの丸め処理どうする
  • equals()の実装
  • hashCode()の実装
  • nullとの等価性比較
  • 他のオブジェクトとの等価性比較
  • 5CHF * 2 = 10CHF
  • DollarとFrancの重複
  • equalsの一般化
  • timesの一般化
  • FrancとDollarを比較する
  • 通貨の概念
  • testFrancMultiplicationを削除する?

第9章 歩幅の調整

ここでは、ついに「通貨」の概念を導入する時が来ました。

テストを書く

+    #[test]
+    fn test_currency() {
+        assert_eq!("USD", Money::dollar(1).currency());
+        assert_eq!("CHF", Money::franc(1).currency());
+    }

このテストを実行すると、currency methodがありませんと怒られます、早速実装して行きます。

実装する

pub struct Money {
    amount: u32,
    currency: &'static str // 追加
}

    pub fn times (&self, multiplier: u32) -> Money {
        Money {
            amount: &self.amount * multiplier,
            currency: &self.currency // 追加
        }
    }
    pub fn equals (&self, target: Money) -> bool {
        self.amount == target.amount
    }
    pub fn dollar (amount: u32) -> Money {
        Money { 
            amount: amount,
            currency: "USD" // 追加
        }
    }
    pub fn franc (amount: u32) -> Money {
        Money { 
            amount: amount,
            currency: "CHF" // 追加
        }
    }
    pub fn currency (&self) -> &'static str { // 追加
        self.currency
    }

ついでに、前回リファクタリングした結果不要になったDollar・Francは消してしまいます。

-impl MoneyTrait for Dollar {
 -    fn new (amount: u32) -> Money {
 -        Money { amount: amount }
 -    }
 -}
 -
 -impl Franc {
 -    pub fn new (amount: u32) -> Money {
 -        Money { amount: amount }
 -}       

差分Commit

最終的なコード

本章終了時点のコードは以下です。

pub struct Money {
    amount: u32,
    currency: &'static str
}

trait MoneyTrait {
    fn new(amount: u32) -> Money;
}

pub struct Dollar {
}

pub struct Franc {
}

impl Money {
    pub fn times (&self, multiplier: u32) -> Money {
        Money {
            amount: &self.amount * multiplier,
            currency: &self.currency
        }
    }
    pub fn equals (&self, target: Money) -> bool {
        self.amount == target.amount
    }
    pub fn dollar (amount: u32) -> Money {
        Money { 
            amount: amount,
            currency: "USD"
        }
    }
    pub fn franc (amount: u32) -> Money {
        Money { 
            amount: amount,
            currency: "CHF"
        }
    }
    pub fn currency (&self) -> &'static str {
        self.currency
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_multiplication() {
        let five = Money::dollar(5);
        assert!(Money::dollar(10).equals(five.times(2)));
        assert!(Money::dollar(15).equals(five.times(3)));
    }
    #[test]
    fn test_equality() {
        assert!(Money::dollar(5).equals(Money::dollar(5)));
        assert!(!Money::dollar(5).equals(Money::dollar(6)));
        assert!(Money::franc(5).equals(Money::franc(5)));
        assert!(!Money::franc(5).equals(Money::franc(6)));
        //TODO: assert!(!Money::franc(5).equals(Money::dollar(5)));
    }
    #[test]
    fn test_franc_multiplication() {
        let five = Money::franc(5);
        assert!(Money::franc(10).equals(five.times(2)));
        assert!(Money::franc(15).equals(five.times(3)));
    }
    #[test]
    fn test_currency() {
        assert_eq!("USD", Money::dollar(1).currency());
        assert_eq!("CHF", Money::franc(1).currency());
    }
}

今時点のToDoList

  • \$5 + 10CHF = \$10
  • \$5 * 2 = \$10
  • amountをprivateにする(すでにprivateだった)
  • Dollarの副作用どうする
  • Moneyの丸め処理どうする
  • equals()の実装
  • hashCode()の実装
  • nullとの等価性比較
  • 他のオブジェクトとの等価性比較
  • 5CHF * 2 = 10CHF
  • DollarとFrancの重複
  • equalsの一般化
  • timesの一般化
  • FrancとDollarを比較する
  • 通貨の概念
  • testFrancMultiplicationを削除する?

第10章 テストに聞いてみる

第10章では、第7章で棚上げにしていた「FrancとDollarを比較する」を通貨を入れたことでようやくテストができるようになったので、実装して行きます。

 実装する

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

差分Commit

最終的なコード

本章終了時点のコードは以下になります。
https://github.com/Khigashiguchi/rust_sutras_tdd/blob/chapter/10/src/lib.rs

今時点のToDoList

  • \$5 + 10CHF = \$10
  • \$5 * 2 = \$10
  • amountをprivateにする(すでにprivateだった)
  • Dollarの副作用どうする
  • Moneyの丸め処理どうする
  • equals()の実装
  • hashCode()の実装
  • nullとの等価性比較
  • 他のオブジェクトとの等価性比較
  • 5CHF * 2 = 10CHF
  • DollarとFrancの重複
  • equalsの一般化
  • timesの一般化
  • FrancとDollarを比較する
  • 通貨の概念
  • testFrancMultiplicationを削除する?

第11章 不要になったら消す

ここまで実装してきて、重複しているようなテストコードは削除して行きます。

テストコード修正

     fn test_equality() {
         assert!(Money::dollar(5).equals(Money::dollar(5)));
         assert!(!Money::dollar(5).equals(Money::dollar(6)));
-        assert!(Money::franc(5).equals(Money::franc(5)));
-        assert!(!Money::franc(5).equals(Money::franc(6)));

-    fn test_franc_multiplication() {
-        let five = Money::franc(5);
-        assert!(Money::franc(10).equals(five.times(2)));
-        assert!(Money::franc(15).equals(five.times(3)));
-    }

最終的なコード

本章終了時点のコードは以下になります。
https://github.com/Khigashiguchi/rust_sutras_tdd/blob/chapter/11/src/lib.rs

今時点のToDoList

  • \$5 + 10CHF = \$10
  • \$5 * 2 = \$10
  • amountをprivateにする(すでにprivateだった)
  • Dollarの副作用どうする
  • Moneyの丸め処理どうする
  • equals()の実装
  • hashCode()の実装
  • nullとの等価性比較
  • 他のオブジェクトとの等価性比較
  • 5CHF * 2 = 10CHF
  • DollarとFrancの重複
  • equalsの一般化
  • timesの一般化
  • FrancとDollarを比較する
  • 通貨の概念
  • testFrancMultiplicationを削除する?

最後に

次回は、書籍「テスト駆動開発」をRustで書く #2 (chapter4...6)にて、 @tatsuya6502さんにコメントいただいたrust流の書き方にリファクタリングいたします。

参考

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5