TDD系列3-TDD过程实例-保龄球单局积分算法

认识了TDD,我们以实际案例过程来更好的学习TDD。

案例需求

保龄球单局积分规则为:
1、保龄球按顺序每轮允许投2个球,投完10轮为1局。
2、每击倒1个瓶得1分。投完一轮将两个球的“所得分”相加,为该轮的“应得分”,10轮依次累计为全局的总分。
3、保龄球运动有统一格式的记分表。第一球将全部瓶击倒时,称为“全中”。该轮所得分为10分。第二球不得再投。但按规则规定, 应奖励下轮两个球的所得分。它们所得分之和为该轮的应得 分。
4、当第一球击倒部分瓶时,应在左边小格内记上被击倒的木 瓶数,作为第一球的所得分。如果第二球将剩余瓶全部击倒,则称为“补中”。该轮所得分亦为10分。按规则规定,应奖励下轮第一球的所得分。它们所得分 之和为该轮的应得分。
5、第10轮全中时,应在同一条球道上继续投守最后两个球结束全局。这两个球的所得分应累计在该局总分内。
6、第10轮为补中时,应在同一条球道上继续投守最后一个球结束全局。这个球的所得分应累计在该局总分内。
一局总分为300分

TDD过程

整个过程用Php实现。
阅读说明:
1、为了简洁,每部分代码实例只包含新增或修改过的函数体。
2、相对于之前新增代码,注释为”新增测试代码“或”新增实现代码“,如果注释只涉及几行,会单独说明;修改按同样方式处理。

step1 一局开始,记分是0

编写测试代码(BowlingGameRecordTest.php):

public function testGameRecord()
    {
        //初始保龄球得分为0
        $this->assertEquals(0,$this->BowlingGameRecord->getGameRecord());
    }

实现代码(BowlingGameRecord.php):

private $gameRecord ;
    public function __construct() {
        $this->gameRecord = 0;
    }
 public function getGameRecord()
    {
        return $this->gameRecord;
    }

step2 测试简单情况,每次投掷得1分,共投掷20次,得20分

测试代码(BowlingGameRecordTest.php):

    public function testGameRecord()
    {
        ...
        
        //新增测试代码
        //每次得1分,共20次,得20分
        for($i=1;$i<=20;$i++)
            $this->BowlingGameRecord->strikeOne(1);
        $this->assertEquals(20, $this->BowlingGameRecord->getGameRecord());
    }

实现代码(BowlingGameRecord.php):

//新增实现代码
public function strikeOne($score)
    {
        $score = intval($score);
        $this->gameRecord += $score;
    }

step3 测试全中,但为了简单,假定第一轮全中,测到第二轮投掷完即可

测试代码(BowlingGameRecordTest.php):

public function testGameRecord()
    {
        ...
        //新增测试代码
        //第一次投掷全中,第2,3次投掷分别得4,5分,共得28分
        $this->BowlingGameRecord->newGame();
        $this->BowlingGameRecord->strikeOne(10);
        $this->BowlingGameRecord->strikeOne(4);
        $this->BowlingGameRecord->strikeOne(5);
        $this->assertEquals(28, $this->BowlingGameRecord->getGameRecord());
    }

实现代码(BowlingGameRecord.php):

public function strikeOne($score)
    {
        $score = intval($score);
        //累计本次投掷得分        
        $this->gameRecord += $score;

		//新增实现代码
        //记录本次投掷得分
        $currentStrikeTimes = count($this->scorePerStrike);
        print $currentStrikeTimes . '\n';
        $this->scorePerStrike[$currentStrikeTimes] = $score;
        
        if($score == 0)
        {
            //第一轮第一次全中,则第一轮下一次(实际没投掷)为0
            if($score == 10)
                $scorePerStrike[$currentStrikeTimes+1] = 0;
        } else {
            //根据是否新一轮做处理
            $isNewOrder = false;
            if($currentStrikeTimes%2 == 0)
                $isNewOrder = true;
            //如果每轮第一次全中,则每轮下一次(实际没投掷)为0
            if($score == 10 && $isNewOrder)
                $this->scorePerStrike[$currentStrikeTimes+1] = 0;
            if($isNewOrder)
            {
        print $currentStrikeTimes;
                if($this->scorePerStrike[$currentStrikeTimes-2] == 10) //上轮全中
                    $this->gameRecord += $score;  //本次分也是奖励分
            } else {
        print $currentStrikeTimes;
                if($this->scorePerStrike[$currentStrikeTimes-3] == 10) //上轮全中
                    $this->gameRecord += $score;  //本次分也是奖励分
            }
        }
            
    }
    public function newGame()
    {
        $this->gameRecord = 0;
        
        if(isset($this->scorePerStrike))
            unset($this->scorePerStrike);
        $this->scorePerStrike = array();
    } 
    //每轮每次投中得分,每两个为1轮,如果每轮第一次全中,则每轮下一次(实际没投掷)为0分
    private    $scorePerStrike ;

step4 测试补中,但为了简单,假定第一轮是补中,测到第二轮投掷完即可

测试代码(BowlingGameRecordTest.php):

public function testGameRecord()
    {
        ...
        //新增测试代码
        //第1,2,3,4次投掷分别得5,5,6,3分(第一轮补中),共得25分
        $this->BowlingGameRecord->newGame();
        $this->BowlingGameRecord->strikeOne(5);
        $this->BowlingGameRecord->strikeOne(5);
        $this->BowlingGameRecord->strikeOne(6);
        $this->BowlingGameRecord->strikeOne(3);
        $this->assertEquals(25, $this->BowlingGameRecord->getGameRecord());
}

实现代码(BowlingGameRecord.php):

public function strikeOne($score)
    {
        $score = intval($score);
        //累计本次投掷得分        
        $this->gameRecord += $score;
        
        //记录本次投掷得分
        $currentStrikeTimes = count($this->scorePerStrike);
//        print $currentStrikeTimes . '\n';
        $this->scorePerStrike[$currentStrikeTimes] = $score;
        
        if($score == 0)
        {
            //第一轮第一次全中,则第一轮下一次(实际没投掷)为0
            if($score == 10)
                $scorePerStrike[$currentStrikeTimes+1] = 0;
        } else {
            //根据是否新一轮做处理
            $isNewOrder = false;
            if($currentStrikeTimes%2 == 0)
                $isNewOrder = true;
            //如果每轮第一次全中,则每轮下一次(实际没投掷)为0
            if($score == 10 && $isNewOrder)
                $this->scorePerStrike[$currentStrikeTimes+1] = 0;
            if($isNewOrder)
            {
                if($this->scorePerStrike[$currentStrikeTimes-2] == 10) //上轮全中
                    $this->gameRecord += $score;  //本次分也是奖励分
                 //新增实现代码:下面4行
                else {
                    if($this->scorePerStrike[$currentStrikeTimes-2] + $this->scorePerStrike[$currentStrikeTimes-1]  == 10) //上轮补中
                        $this->gameRecord += $score;  //本次分也是奖励分
                }
                    
            } else {
                if($this->scorePerStrike[$currentStrikeTimes-3] == 10) //上轮全中
                    $this->gameRecord += $score;  //本次分也是奖励分
            }
        }

step5 测试特殊情况,满分300分情况

测试代码(BowlingGameRecordTest.php):

 public function testGameRecord()
    {
        ...
        //新增测试代码
        //所有全中,共得300分
        $this->BowlingGameRecord->newGame();
        for($i=1;$i<=12;$i++)
            $this->BowlingGameRecord->strikeOne(10);
        $this->assertEquals(300, $this->BowlingGameRecord->getGameRecord());
}

实现代码(BowlingGameRecord.php):

public function strikeOne($score)
    {
        $score = intval($score);
        //修订实现代码:下面6行
        //记录本次投掷得分
        $currentStrikeTimes = count($this->scorePerStrike);
        $this->scorePerStrike[$currentStrikeTimes] = $score;
        //累计本次投掷得分(注意10轮之后只记录奖励分)
        if($currentStrikeTimes < 20)        
            $this->gameRecord += $score;
                
        if($score == 0)
        {
            //第一轮第一次全中,则第一轮下一次(实际没投掷)为0
            if($score == 10)
                $scorePerStrike[$currentStrikeTimes+1] = 0;
        } else {
            //根据是否新一轮做处理
            $isNewOrder = false;
            if($currentStrikeTimes%2 == 0)
                $isNewOrder = true;
            //如果每轮第一次全中,则每轮下一次(实际没投掷)为0
            if($score == 10 && $isNewOrder)
                $this->scorePerStrike[$currentStrikeTimes+1] = 0;
            if($isNewOrder)
            {
                //上轮全中
                if($this->scorePerStrike[$currentStrikeTimes-2] == 10) 
                {
                    //修订实现代码:下面5行
                    if($currentStrikeTimes-2 < 20)
                        $this->gameRecord += $score;  //本次分也是奖励分
                    if($currentStrikeTimes-4>=0 && 
                    $this->scorePerStrike[$currentStrikeTimes-4] == 10) //上两轮全中
                        $this->gameRecord += $score;  //本次分也是奖励分
                }
                else {
                    if($this->scorePerStrike[$currentStrikeTimes-2] + $this->scorePerStrike[$currentStrikeTimes-1]  == 10) //上轮补中
                        $this->gameRecord += $score;  //本次分也是奖励分
                }
                    
            } else {
                if($this->scorePerStrike[$currentStrikeTimes-3] == 10) //上轮全中
                    $this->gameRecord += $score;  //本次分也是奖励分
            }
        }
            
    }

step6 测试一个正常投掷情况

测试代码(BowlingGameRecordTest.php):

public function testGameRecord()
    {
        ...
        //新增测试代码
        //随机一局:6,3 10 10 10 5,2 8,2 8,1 9,0 10 5,5 2,共得156分
        $this->BowlingGameRecord->newGame();
        $this->BowlingGameRecord->strikeOne(6);
        $this->BowlingGameRecord->strikeOne(3);
        $this->BowlingGameRecord->strikeOne(10);
        $this->BowlingGameRecord->strikeOne(10);
        $this->BowlingGameRecord->strikeOne(10);
        $this->BowlingGameRecord->strikeOne(5);
        $this->BowlingGameRecord->strikeOne(2);
        $this->BowlingGameRecord->strikeOne(8);
        $this->BowlingGameRecord->strikeOne(2);
        $this->BowlingGameRecord->strikeOne(8);
        $this->BowlingGameRecord->strikeOne(1);
        $this->BowlingGameRecord->strikeOne(9);
        $this->BowlingGameRecord->strikeOne(0);
        $this->BowlingGameRecord->strikeOne(10);
        $this->BowlingGameRecord->strikeOne(5);
        $this->BowlingGameRecord->strikeOne(5);
        $this->BowlingGameRecord->strikeOne(2);
        $this->assertEquals(156, $this->BowlingGameRecord->getGameRecord());
}

实现代码(BowlingGameRecord.php):
正确,不用修改

step7 重构BowlingGameRecord,重构中发现一个Bug

测试代码(BowlingGameRecordTest.php):
不用修改

实现代码(BowlingGameRecord.php):

public function strikeOne($score)
    {
        $score = intval($score);
        //记录本次投掷得分
        $currentStrikeTimes = count($this->scorePerStrike);
        $this->scorePerStrike[$currentStrikeTimes] = $score;

		//(重构)修订实现代码:源代码简化如下
        //如果每轮第一次全中,则每轮下一次(实际没投掷)为0
        if($currentStrikeTimes%2 == 0 && $score == 10)
                $this->scorePerStrike[$currentStrikeTimes+1] = 0;
        
        //累计本次投掷得分(注意10轮之后只记录奖励分)
        if($currentStrikeTimes < 20)        
            $this->gameRecord += $score;
        
        //奖励分处理
        $this->rewardPerStrike($currentStrikeTimes,$score);
        
    }
    //(重构)新增实现代码
    private    function rewardPerStrike($currentStrikeTimes,$score)
    {
            $currentStrikeTimes = intval($currentStrikeTimes);
            $score = intval($score);
            if($currentStrikeTimes == 0)
                return;

            //新一轮第一次投掷标识
            if($currentStrikeTimes%2 == 0)
            {
                //上轮全中
                if($this->scorePerStrike[$currentStrikeTimes-2] == 10) 
                {
                    if($currentStrikeTimes-2 < 20)
                        $this->gameRecord += $score;  //本次分是上一轮全中奖励分
                    if($currentStrikeTimes-4>=0 && 
                    $this->scorePerStrike[$currentStrikeTimes-4] == 10) //上两轮全中
                        $this->gameRecord += $score;  //本次分是上一轮的上一轮全中奖励分
                }
                else {
                    if($this->scorePerStrike[$currentStrikeTimes-2] + $this->scorePerStrike[$currentStrikeTimes-1]  == 10) //上轮补中
                        $this->gameRecord += $score;  //本次分是全中奖励分
                }
                    
            } else {  
                //每一轮第二次投掷标识
                if($this->scorePerStrike[$currentStrikeTimes-3] == 10) //上轮全中
                    $this->gameRecord += $score;  //本次分是全中奖励分
            }
            print ' $currentStrikeTimes=' . $currentStrikeTimes . ' gameRecord=' . $this->gameRecord;
            
    }

至此,用TDD开发保龄球单局记分完成。在整个开发过程中,大家可以看到每个步骤增加代码都不多,属于小步快跑;运行一旦发现之前的测试用例出错,马上进行修订,包括最后重构,都让人感觉很有把握;从效率上来说,前后总共用时2:50分钟,还算比较快的;有兴趣的同仁可以试试用自己最熟悉的方式开发,最后能编出正确的保龄球单局积分程序大约花多少时间。

实践体验总结

  1. 从简单入手,快速开始,并逐步找到解决问题的方法。
  2. 看到绿色条就开心,总觉得又前进了一步。
  3. 看到测试红色条就马上紧张,特别是之前通过的测试又不通过了,但由于步骤不大,解决问题没有很高难度,能快速解决问题。
  4. 迫使你不端重构代码,但由于有之前测试保证,重构比较大胆并放心 。
  5. 运行测试频繁,基本只要上改过代码,就运行一次,但感觉会更好。
  6. 重构过程中,可能很快会撤消刚不久前所做的工作,但并不给人做无用功的感觉,而是“原来应该这样做”的想法。
  7. 对代码有信心,无多余代码,测试覆盖率高。
  8. 在做过程中,可随需中断,之后可快速接着开始 。
  9. 相比于正常编码+测试+修改bug,感觉TDD所用时间能节省。
  10. 增加代码量还是不少,但在可接受范围内。

组织级TDD实践如何落地,见“企业如何落地TDD"。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
测试驱动的编程是 XP 困扰程序员的一个方面。对于测试驱动的编程意味着什么以及如何去做,大多数人都做出了不正确的假设。这个月,XP 方面的讲师兼 Java 开发人员 Roy Miller 谈论了测试驱动的编程是什么,它为什么可以使程序员的生产力和质量发生巨大变化,以及编写测试的原理。请在与本文相随的 论坛中提出您就本文的想法,以飨笔者和其他读者。(您也可以单击本文顶部或底部的“讨论”来访问该论坛。) 最近 50 年来,测试一直被视为项目结束时要做的事。当然,可以在项目进行之中结合测试,测试通常并不是在 所有编码工作结束后才开始,而是一般在稍后阶段进行测试。然而,XP 的提倡者建议完全逆转这个模型。作为一名程序员,应该在编写代码 之前编写测试,然后只编写足以让测试通过的代码即可。这样做将有助于使您的系统尽可能的简单。 先编写测试 XP 涉及两种测试: 程序员测试和 客户测试。测试驱动的编程(也称为 测试为先编程)最常指第一种测试,至少我使用这个术语时是这样。测试驱动的编程是让 程序员测试(即单元测试 ― 重申一下,只是换用一个术语)决定您所编写的代码。这意味着您必须在编写代码之前进行测试。测试指出您 需要编写的代码,从而也 决定了您要编写的代码。您只需编写足够通过测试的代码即可 ― 不用多,也不用少。XP 规则很简单:如果不进行程序员测试,则您不知道要编写什么代码,所以您不会去编写任何代码。 测试驱动开发(TDD)是极限编程的重要特点,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量。本文从开发人员使用的角度,介绍了 TDD 优势、原理、过程、原则、测试技术、Tips 等方面。 背景 一个高效的软件开发过程对软件开发人员来说是至关重要的,决定着开发是痛苦的挣扎,还是不断进步的喜悦。国人对软件蓝领的不屑,对繁琐冗长的传统开发过程的不耐,使大多数开发人员无所适从。最近兴起的一些软件开发过程相关的技术,提供一些比较高效、实用的软件过程开发方法。其中比较基础、关键的一个技术就是测试驱动开发(Test-Driven Development)。虽然TDD光大于极限编程,但测试驱动开发完全可以单独应用。下面就从开发人员使用的角度进行介绍,使开发人员用最少的代价尽快理解、掌握、应用这种技术。下面分优势,原理,过程,原则,测试技术,Tips等方面进行讨论。 1. 优势 TDD的基本思路就是通过测试来推动整个开发的进行。而测试驱动开发技术并不只是单纯的测试工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乐享技术

每一个打赏,都是对我最大的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值