これからはじめるTDD

tatsu-zine.com

C# で写経してみた。
Testプロジェクトは、頭に Test をつけたけど、後ろにつけるべきだったのかな。
注意点は、プロダクトの方にテストプロジェクトがアクセスするためには、プロダクト側の AssemblyInfo.cs に以下を記述すること。

[assembly: InternalsVisibleTo("TestKorekaraTdd")]

BowlingGame.cs

using System.Collections.Generic;
using System.Linq;

namespace KorekaraTdd
{
    class BowlingGame
    {
        private Frame spareFrame;
        private List<Frame> strikes = new List<Frame> { };
        private IList<Frame> frames = new List<Frame> { new Frame() };

        public int FrameScore(int frameNo)
        {
            return frames[frameNo - 1].Score;
        }

        public void RecordShot(int pins)
        {
            frames.Last().RecordShot(pins);
            CalcSpareBonus(pins);
            CalcStrikeBonus(pins);
            if (frames.Last().IsFinished())
            {
                frames.Add(new Frame());
            }
        }

        public int Score()
        {
            int total = 0;
            foreach(var frame in frames)
            {
                total += frame.Score;
            }
            return total;
        }

        private void AddStrikeBonus(int pins)
        {
            foreach(var strike in strikes)
            {
                if (strike.IsNeedBonus())
                {
                    strike.AddBonus(pins);
                }
            }
        }

        private void CalcSpareBonus(int pins)
        {
            if (spareFrame != null && spareFrame.IsNeedBonus())
            {
                spareFrame.AddBonus(pins);
            }

            if (frames.Last().IsSpare())
            {
               spareFrame = frames.Last();
            }
        }

        private void CalcStrikeBonus(int pins)
        {
            AddStrikeBonus(pins);
            if (frames.Last().IsStrike())
            {
                RecognizeStrikeBonus();
            }
        }

        private void RecognizeStrikeBonus()
        {
            strikes.Add(frames.Last());
        }
    }
}

Frame.cs

using System;

namespace KorekaraTdd
{
    class Frame
    {
        private int score = 0;
        private int bonusCount = 0;

        public int Score {
            get
            {
                return score + Bonus;
            }
            private set
            {
                score = value;
            }
        }

        public int Bonus { get; private set; }

        private int shotCount = 0;

        public Frame()
        {
            Score = 0;
            Bonus = 0;
        }

        public void AddBonus(int bonus)
        {
            Bonus += bonus;
            bonusCount++;
        }

        public bool IsFinished()
        {
            return Score >= 10 || shotCount > 1;
        }

        public bool IsSpare()
        {
            return Score >= 10 && shotCount > 1;
        }

        public bool IsStrike()
        {
            return Score >= 10 && shotCount == 1;
        }

        public bool IsNeedBonus()
        {
            if (IsSpare())
            {
                return bonusCount < 1;
            }

            if (IsStrike())
            {
                return bonusCount < 2;
            }

            return false;
        }

        public void RecordShot(int pins)
        {
            CheckPinsCount(pins);
            Score += pins;
            shotCount++;
        }

        private void CheckPinsCount(int pins)
        {
            if ((pins < 0 || 10 < pins) && Score + pins <= 10)
            {
                throw new ArgumentException();
            }
        }
    }
}

BowlingGameTest.cs

using System;
using KorekaraTdd;

namespace TestKorekaraTdd
{
    using NUnit.Framework;

    [TestFixture]
    public class BowlingGameTest
    {
        BowlingGame game;

        [SetUp]
        public void SetUp()
        {
            game = new BowlingGame();
        }

        [Test]
        public void 全ての投球がガター()
        {
            RecordManyShot(20, 0);
            Assert.That(game.Score, Is.EqualTo(0));
        }

        [Test]
        public void 全ての投球で1ピンだけ倒した()
        {
            RecordManyShot(20, 1);
            Assert.That(game.Score, Is.EqualTo(20));
        }

        [Test]
        public void スペアをとると次の投球のピン数を加算()
        {
            RecordSpare();
            game.RecordShot(4);
            RecordManyShot(17, 0);

            Assert.That(game.Score, Is.EqualTo(18));
            Assert.That(game.FrameScore(1), Is.EqualTo(14));
        }

        [Test]
        public void 直前の投球との合計が10品でもフレーム違いはスペアではない()
        {
            foreach (var i in new int[] { 2, 5, 5, 1 })
            {
                game.RecordShot(i);
            }
            RecordManyShot(16, 0);
            Assert.That(game.Score, Is.EqualTo(13));
        }

        [Test]
        public void ストライクをとると次の2投分のピン数を加算()
        {
            RecordStrike();
            foreach (var i in new int[] { 3, 3, 1 })
            {
                game.RecordShot(i);
            }
            RecordManyShot(15, 0);
            Assert.That(game.Score, Is.EqualTo(23));
            Assert.That(game.FrameScore(1), Is.EqualTo(16));
        }

        [Test]
        public void 連続ストライクすなわちダブル()
        {
            RecordStrike();
            RecordStrike();
            foreach (var i in new int[] { 3, 1 })
            {
                game.RecordShot(i);
            }
            RecordManyShot(14, 0);
            Assert.That(game.Score, Is.EqualTo(41));
            Assert.That(game.FrameScore(1), Is.EqualTo(23));
            Assert.That(game.FrameScore(2), Is.EqualTo(14));
        }

        [Test]
        public void 連続3回ストライクすなわちターキー()
        {
            for (var i = 0; i < 3; i++) { RecordStrike(); }
            foreach (var i in new int[] { 3, 1 })
            {
                game.RecordShot(i);
            }
            RecordManyShot(12, 0);
            Assert.That(game.Score, Is.EqualTo(71));
        }

        [Test]
        public void ストライクの後のスペア()
        {
            RecordStrike();
            RecordSpare();
            game.RecordShot(3);
            RecordManyShot(15, 0);
            Assert.That(game.Score, Is.EqualTo(36));
        }

        [Test]
        public void ダブル後のスペア()
        {
            RecordStrike();
            RecordStrike();
            RecordSpare();
            game.RecordShot(3);
            RecordManyShot(13, 0);
            Assert.That(game.Score, Is.EqualTo(61));
        }

        [Test]
        public void 全ての投球がガターの場合の第1フレームの得点()
        {
            RecordManyShot(20, 0);
            Assert.That(game.FrameScore(1), Is.EqualTo(0));
        }

        [Test]
        public void 全ての投球が1ピンだと全フレーム2点()
        {
            RecordManyShot(20, 1);
            Assert.That(game.FrameScore(1), Is.EqualTo(2));
        }

        private void RecordManyShot(int count, int pins)
        {
            for (var i = 0; i < count; i++)
            {
                game.RecordShot(pins);
            }
        }

        private void RecordStrike()
        {
            game.RecordShot(10);
        }

        private void RecordSpare()
        {
            RecordManyShot(2, 5);
        }
    }
}

FrameTest.cs

using System;
using KorekaraTdd;

namespace TestKorekaraTdd
{
    using NUnit.Framework;

    [TestFixture]
    class FrameTest
    {
        Frame frame;

        [SetUp]
        public void SetUp()
        {
            frame = new Frame();
        }

        [Test]
        public void 全ての投球がガター()
        {
            frame.RecordShot(0);
            frame.RecordShot(0);
            Assert.That(frame.Score, Is.EqualTo(0));
        }

        [Test]
        public void 全ての投球で1ピンだけ倒した()
        {
            frame.RecordShot(1);
            frame.RecordShot(1);
            Assert.That(frame.Score, Is.EqualTo(2));
        }

        [Test]
        public void 二投するとフレームは完了する()
        {
            frame.RecordShot(1);
            Assert.That(frame.IsFinished(), Is.False);
            frame.RecordShot(1);
            Assert.That(frame.IsFinished(), Is.True);
        }

        [Test]
        public void 十ピン倒した時点でフレームは完了する()
        {
            frame.RecordShot(10);
            Assert.That(frame.IsFinished, Is.True);
        }

        [Test]
        public void 二投目で10ピン倒すとスペア()
        {
            frame.RecordShot(5);
            Assert.That(frame.IsSpare(), Is.False);
            frame.RecordShot(5);
            Assert.That(frame.IsSpare(), Is.True);
        }

        [Test]
        public void 一投目で10ピン倒すとストライク()
        {
            Assert.That(frame.IsStrike(), Is.False);
            frame.RecordShot(10);
            Assert.That(frame.IsStrike(), Is.True);
        }

        [Test]
        public void ボーナス点を加算する()
        {
            frame.RecordShot(5);
            frame.RecordShot(5);
            frame.AddBonus(5);
            Assert.That(frame.Score, Is.EqualTo(15));
        }

        [Test]
        public void オープンフレームにはボーナス不要()
        {
            frame.RecordShot(3);
            frame.RecordShot(3);
            Assert.That(frame.IsNeedBonus(), Is.False);
        }

        [Test]
        public void スペアのボーナスは1投分で完了()
        {
            frame.RecordShot(5);
            frame.RecordShot(5);
            Assert.That(frame.IsNeedBonus(), Is.True);
            frame.AddBonus(5);
            Assert.That(frame.IsNeedBonus(), Is.False);
        }

        [Test]
        public void ストライクのボーナスは2投分で完了()
        {
            frame.RecordShot(10);
            frame.AddBonus(5);
            Assert.That(frame.IsNeedBonus(), Is.True);
            frame.AddBonus(5);
            Assert.That(frame.IsNeedBonus(), Is.False);
        }

        [Test]
        public void ピン数は0本以上()
        {
            Assert.Throws<ArgumentException>( delegate { frame.RecordShot(-1); });
        }
    }
}