LoginSignup
11
1

More than 5 years have passed since last update.

書籍「テスト駆動開発」をRustで書く #2 (chapter4...6)

Last updated at Posted at 2017-12-08

はじめに

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

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

目次

  • 概要
  • 前回のおさらい
  • 第4章 意図を語るテスト
  • 第5章 原則をあえて破るとき
  • 第6章 テスト不足に気づいたら
  • 最後に

概要

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

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

前回のおさらい

前回時点のToDoList

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

  • [ ]$5 + 10CHF = $10
  • [x]$5 * 2 = $10
  • [ ]amountをprivateにする
  • [x]Dollarの副作用どうする
  • [ ]Moneyの丸め処理どうする
  • [x]equals()の実装
  • [ ]hashCode()の実装

前回時点でのコード

第4章 意図を語るテスト

前回書いたコードにて、times()はDollarを返すことになった。テストコードでもその仕様に明示的にわかるようにしたい。

    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);
    }

テストを修正する

Dollar同士を比較するequal()を実装したのでそのメソッドを利用します。

-        assert_eq!(Dollar::new(10).amount, product.amount);
+        assert!(Dollar::new(10).equals(product));

差分Commit

上記の修正の結果、テストコードに不要な変数ができましたのでリファクタリングします。

-        let mut product = five.times(2);
-        assert!(Dollar::new(10).equals(product));
-        product = five.times(3);
-        assert!(Dollar::new(15).equals(product));
+        assert!(Dollar::new(10).equals(five.times(2)));
+        assert!(Dollar::new(15).equals(five.times(3)));

差分Commit

テストが通ればOKです!

最終的なコード

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

#[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);
        assert!(Dollar::new(10).equals(five.times(2)));
        assert!(Dollar::new(15).equals(five.times(3)));
    }
    #[test]
    fn test_equality() {
        assert!(Dollar::new(5).equals(Dollar::new(5)));
        assert!(!Dollar::new(5).equals(Dollar::new(6)));
    }
}

この時点でのToDoList

  • [ ]$5 + 10CHF = $10
  • [x]$5 * 2 = $10
  • [x]amountをprivateにする (すでにprivateだった...)
  • [x]Dollarの副作用どうする
  • [ ]Moneyの丸め処理どうする
  • [x]equals()の実装
  • [ ]hashCode()の実装

第5章 原則をあえて破るとき

現在、Dollarは表現できているが、多国籍通貨での計算を実現するにあたり他の通貨も表現しないといけない。
第一歩として、フラン(Franc)での足し算をテストから書いていく。

ToDoListを追加

  • [x]5CHF * 2 = 10CHF

テストを書く

すでに書いているtest_multiplicationをそのまま流用してfrancの足し算を行うテストを書きます。

    #[test]
    fn test_franc_multiplication() {
        let five = Franc::new(5);
        assert!(Franc::new(10).equals(five.times(2)));
        assert!(Franc::new(15).equals(five.times(3)));
    }

テストを通す実装する

DollarをコピーしてFrancを作ります。重複を生み出す大罪だとしても、まずはテストを通してリファクタリングしていく道を行きます。

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

差分Commit

無事テストが通りました。
今回の大罪を犯した結果生まれた新たなToDoListを追加して本章は終了です。

最終的なコード

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

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

pub struct Franc {
    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
    }
}

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

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

今時点でのToDoList

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

第6章 テスト不足に気づいたら

第5章で、重複したDollarとFrancを作ってしまいました。綺麗にリファクタリングするにあたり、概念上親に当たるMoneyオブジェクトを作る方法を行ってみる。

Moneyオブジェクトを実装する

すでにテストコードを前回書いているもののリファクタリングになるため、実装から始めます。

  • DollarやFrancが継承するTraitを作る
trait MoneyTrait {
    fn new(amount: u32) -> Money;
    fn times(&self, multiplier: u32) -> Money;
    fn equals(&self, target: Money) -> bool;
}
  • Dollarをtraitに合わせて修正する
impl MoneyTrait for Dollar {
    fn new (amount: u32) -> Money {
        Money { amount: amount }
    }
    fn times (&self, multiplier: u32) -> Money {
        Money {amount: self.amount * multiplier }
    }
    fn equals (&self, target: Money) -> bool {
        self.amount == target.amount
    }
}

Dollarでは、Moneyを返り値として渡すように修正しています。

  • Moneyオブジェクトを作る
struct Money {
    amount: u32
}

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

差分Commit

これで一回テストが通るはずです。

差分Commit内にはMoneyというTraitを削除している記述があります。
これは私自身が最初Traitを利用した結果コード量の削減に繋がらなさそうだと考え、別オブジェクトをreturnする上記の方法に変更した経緯で発生している差分になります。

重複を削除する

「Moneyオブジェクトを実装する」にて、times()とequals()はMoney内のものを使うようになったので、Dollarからメソッドを引き上げます。

差分Commit

Francも同じく

Dollarから重複を引き上げられたので、Francも同じやり方をしたいです。でも、等価性のテストでFrancがかけていないことに気がつきました。追加します。

  • テストを追加する
+        assert!(Franc::new(5).equals(Franc::new(5)));
+        assert!(!Franc::new(5).equals(Franc::new(6)));

最終的なコード

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

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
    }
}

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 = Dollar::new(5);
        assert!(Dollar::new(10).equals(five.times(2)));
        assert!(Dollar::new(15).equals(five.times(3)));
    }
    #[test]
    fn test_equality() {
        assert!(Dollar::new(5).equals(Dollar::new(5)));
        assert!(!Dollar::new(5).equals(Dollar::new(6)));
        assert!(Franc::new(5).equals(Franc::new(5)));
        assert!(!Franc::new(5).equals(Franc::new(6)));
    }
    #[test]
    fn test_franc_multiplication() {
        let five = Franc::new(5);
        assert!(Franc::new(10).equals(five.times(2)));
        assert!(Franc::new(15).equals(five.times(3)));
    }
}

今時点でのToDoList

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

実は、原著ではtimes()の一般化はここではやっていないのですが、実装都合上こちらで完了させました。

最後に

次回は、第7章以降を進めて行きます。

参考

11
1
9

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
11
1