认识了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分钟,还算比较快的;有兴趣的同仁可以试试用自己最熟悉的方式开发,最后能编出正确的保龄球单局积分程序大约花多少时间。
实践体验总结
- 从简单入手,快速开始,并逐步找到解决问题的方法。
- 看到绿色条就开心,总觉得又前进了一步。
- 看到测试红色条就马上紧张,特别是之前通过的测试又不通过了,但由于步骤不大,解决问题没有很高难度,能快速解决问题。
- 迫使你不端重构代码,但由于有之前测试保证,重构比较大胆并放心 。
- 运行测试频繁,基本只要上改过代码,就运行一次,但感觉会更好。
- 重构过程中,可能很快会撤消刚不久前所做的工作,但并不给人做无用功的感觉,而是“原来应该这样做”的想法。
- 对代码有信心,无多余代码,测试覆盖率高。
- 在做过程中,可随需中断,之后可快速接着开始 。
- 相比于正常编码+测试+修改bug,感觉TDD所用时间能节省。
- 增加代码量还是不少,但在可接受范围内。
组织级TDD实践如何落地,见“企业如何落地TDD"。