『テスト駆動開発』第1部をC#で写経してみる(第3章)

第3章 三角測量

第3章ではValue Objectを作成し、別名参照問題を防ぐ方法を見ていく。

まずはDollarオブジェクトの等価性について確認する。

namespace TddStudyTest
{
    [TestClass]
    public class MoneyTest
    {
        [TestMethod]
        public void TestMultiplication()
        {
            Dollar five = new Dollar(5);
            Dollar product = five.times(2);
            Assert.AreEqual(10, product.amount);
            product = five.times(3);
            Assert.AreEqual(15, product.amount);
        }

        [TestMethod]
        public void TestEquality()
        {
            Assert.IsTrue(new Dollar(5).Equals(new Dollar(5)));
        }
    }
}

TestEquality()を追加した。当然のごとくテストは失敗するので、trueを返すだけの仮実装を行う。

namespace TddStudy.Money
{
    public class Dollar
    {
        public int amount;

        public Dollar(int amount)
        {
            this.amount = amount;
        }

        public Dollar times(int multiplier)
        {
            return new Dollar(this.amount * multiplier);
        }

        public override bool Equals(object obj)
        {
            return true;
        }
    }
}

テストが通ったので、今度は別の等価性確認を行ってみる。

namespace TddStudyTest
{
    [TestClass]
    public class MoneyTest
    {
        [TestMethod]
        public void TestMultiplication()
        {
            Dollar five = new Dollar(5);
            Dollar product = five.times(2);
            Assert.AreEqual(10, product.amount);
            product = five.times(3);
            Assert.AreEqual(15, product.amount);
        }

        [TestMethod]
        public void TestEquality()
        {
            Assert.IsTrue(new Dollar(5).Equals(new Dollar(5)));
            Assert.IsFalse(new Dollar(5).Equals(new Dollar(6)));
        }
    }
}

追加したテストが失敗するので、ここで透過性比較を一般化する必要性が見えてくる。

namespace TddStudy.Money
{
    public class Dollar
    {
        public int amount;

        public Dollar(int amount)
        {
            this.amount = amount;
        }

        public Dollar times(int multiplier)
        {
            return new Dollar(this.amount * multiplier);
        }

        public override bool Equals(object obj)
        {
            var dollar = (Dollar)obj;
            return amount == dollar.amount;
        }
    }
}

equals()をoverrideしたときはHashCode()もoverrideする必要があるが、今の時点ではスルーする。

このように、2つの異なる視点からのテストメソッドを作成することにより、コードの一般化を行うことができた。『テスト駆動開発』では、この手法を三角測量と呼んでいる。

雑感

三角測量を用いてコードの一般化を行うケースは少ないかもしれないが、本書に書いてある通り、どうやってリファクタリングしてよいか分からないときや、設計に迷ったときは重宝しそうだ。

『テスト駆動開発』第1部をC#で写経してみる(第2章)

第2章 明白な実装 (Degenerate Objects)

第1章に引き続き進めていく。

Dollarオブジェクトの times メソッドに以前とは異なる引数を渡し、期待値が得られるかを確認したい。現段階ではどう考えてもテストは失敗するのだが、失敗させるのが重要なのでテストを追加してみる。

namespace TddStudyTest
{
    [TestClass]
    public class MoneyTest
    {
        [TestMethod]
        public void TestMultiplication()
        {
            Dollar five = new Dollar(5);
            five.times(2);
            Assert.AreEqual(10, five.amount);
            five.times(3);
            Assert.AreEqual(15, five.amount);
        }
    }
}

テストを実行し、失敗することを確認後、即座にグリーンバーを目指すため、times メソッドに対して以下の修正を行う。

  • 戻り値をDollarクラスに変更
  • multiplieramout を掛け合わせて新しい金額のDollarオブジェクトを返す
namespace TddStudy.Money
{
    public class Dollar
    {
        public int amount;

        public Dollar(int amount)
        {
            this.amount = amount;
        }

        public Dollar times(int multiplier)
        {
            return new Dollar(this.amount * multiplier);
        }
    }
}

上記修正に伴いテストは以下のようになる。

namespace TddStudyTest
{
    [TestClass]
    public class MoneyTest
    {
        [TestMethod]
        public void TestMultiplication()
        {
            Dollar five = new Dollar(5);
            Dollar product = five.times(2);
            Assert.AreEqual(10, product.amount);
            product = five.times(3);
            Assert.AreEqual(15, product.amount);
        }
    }
}

これでグリーンバーが復活した。

雑感

第1章ではベタ書きの値を徐々に変数に置き換えていく仮実装を行ったが、今回はいきなり実装に入る明白な実装を行った。具体例から徐々に抽象度を上げていく仮実装の方がセーフティーということか。

しかしテストコードの修正から入るというのにはまだまだ慣れそうにない。

『テスト駆動開発』第1部をC#で写経してみる(第1章)

はじめに

和田卓人さん訳の『テスト駆動開発』を以前に読了していたが、ふと思い立ったので復習がてら第1部の内容をC#で写経してみることにした。第1部は債権ポートフォリオ管理システムを多国通貨対応させる、というシナリオで話が進む。なお、写経に際して前提条件や細かい説明は省くので、詳細は実際に『テスト駆動開発』を読んで確認してみてほしい。

また、第1部は全部で17章あるので、実際にコードを書きながらチマチマ進めていきたい。

写経環境

言語

いわずもがなC#

IDE

Visual Studio 2017 Professional

テスティングフレームワーク

MSTest v2

第1章 仮実装 (Multi-Currency Money)

いきなりテストコードを書いてみる。

namespace TddStudyTest
{
    [TestClass]
    public class MoneyTest
    {
        [TestMethod]
        public void TestMultiplication()
        {
            Dollar five = new Dollar(5);
            five.times(2);
            Assert.AreEqual(10, five.amount);
        }
    }
}

この時点ではコンパイルエラーが発生する。Dollarクラスなど存在しないので当然だ。

そこでDollarクラスを作成し、times メソッドと amount フィールドを追加する。

namespace TddStudy.Money
{
    public class Dollar
    {
        public int amount;

        public Dollar(int amount)
        {
        }

        public void times(int multiplier)
        {
        }
    }
}

コンパイルエラーは取れたが、この状態でテストを実行させるとエラーになるので、最小限の対応を行ってみる。

namespace TddStudy.Money
{
    public class Dollar
    {
        public int amount = 10;

        public Dollar(int amount)
        {
        }

        public void times(int multiplier)
        {
        }
    }
}

10を2と5に分解し、テストコードと値を重複させてみる。

namespace TddStudy.Money
{
    public class Dollar
    {
        public int amount;

        public Dollar(int amount)
        {
        }

        public void times(int multiplier)
        {
            this.amount = 5 * 2;
        }
    }
}

仕上げにコンストラクタ内で amout の代入を行い、 times メソッド内で amout フィールドを参照するようにする。

namespace TddStudy.Money
{
    public class Dollar
    {
        public int amount;

        public Dollar(int amount)
        {
            this.amount = amount;
        }

        public void times(int multiplier)
        {
            this.amount *= multiplier;
        }
    }
}

これで一旦テストがパスできる状態になった。めでたしめでたし…ではないので、第2章に続く。

雑感

テスト対象クラスが存在しない状態からテストコードを書くことが非常に新鮮な体験だった。
小刻みな修正を行って一歩ずつ歩みを進めつつ、テストをパスしてグリーンバーを眺める安心感は何物にも代えがたいが、仮初めのグリーンバーなことも多々ある点に要注意。