TDD实践初体验
本文将分成以下四部分来阐述TDD
- 什么是TDD开发
- 为什么要采用TDD开发
- 怎么落地执行TDD开发
- 如何做好TDD开发
什么是TDD
TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术, 也是一种设计方法论。 TDD的基本思路就是通过测试来推动整个开发的进行,但测试驱动开发并不只是单纯的测试工作, 而是把需求分析,设计,质量控制量化的过程。
为什么要采用TDD开发
如今的互联网产品更新迭代速度很快,用户需求变化频繁,而采用TDD模式开发的产品, 可以在任意一个开发节点都可以拿出一个可以使用,含少量bug并具一定功能和能够发布的产品。
如何执行TDD开发
TDD的基本流程简单概括就是:红、绿、重构。
- 写一个测试用例
- 运行测试
- 写刚好能让测试通过的实现
- 运行测试
- 识别坏味道,重构优化代码
- 运行测试
如何做好TDD开发
遵循TDD的三条原则
- 除非是为了使一个失败的测试通过,否则不允许编写任何产品代码
- 在一个单元测试中,只允许编写刚好能够导致失败的内容
- 只允许编写刚好能够使一个失败的测试通过的产品代码
合理拆分任务
TDD之前要拆分任务,把一个大的需求拆分成多个小需求,每个需求尽量简单
编写有效的单元测试
一个好的单元测试必然符合以下特点:
- 简单一次只测一个需求,
- 符合Given-When-Then格式(一个上下文,指定测试预设,进行一系列操作,即所要执行的操作,得到一系列可观察的后果)
- 包含断言,可以重复执行。
接下来结合案例来实践下TDD
需求:给定一个整数,将其转为罗马数字。输入确保在 1 到 3999 的范围内。
| roman number | arabic number |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
- I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
- X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
- C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
利用TDD思想实现该需求的步骤如下:
第一步分析需求
- 接受一个参数且为数值类型
- 参数的约束条件是1-3999之间
- 输出值是转换后的罗马数字
我们可以把先把这个需求分解成多个小需求,比如我们可以把这个需求分解成如下小需求
- 处理10以内一位数的情况
- 处理100以内两位数的情况
- 处理1000以内三位数的情况
- 处理3999以内四位数的情况
第二步设计测试用例
需求分析分解完了,我们首要针对子需求1用TDD的方式实现
设计测试用例如下
use PHPUnit\Framework\TestCase;
class NumberConverTest extends TestCase
{
/**
* @dataProvider lessThanTenProvider
* @test
*/
public function it_can_conver_arabic_to_roman_when_number_less_then_ten($arabic, $roman)
{
$this->assertEquals($roman, NumberConver::arabicNumberToRomanNumber($arabic));
}
public function lessThanTenProvider()
{
return [
'echo I when input 1' => [1, 'I'],
'echo II when input 2' => [2, 'II'],
'echo III when input 3' => [3, 'III'],
'echo IV when input 4' => [4, 'IV'],
'echo V when input 5' => [5, 'V'],
'echo VI when input 6' => [6, 'VI'],
'echo VII when input 7' => [7, 'VII'],
'echo VIII when input 8' => [8, 'VIII'],
'echo IX when input 9' => [9, 'IX'],
];
}
}
运行测试,返回结果如下
Call to undefined method App\Library\NumberConver::arabicNumberToRomanNumber()
因为此时我们尚未实现任何业务逻辑
第三步写刚好能让测试通过的业务逻辑
针对这个测试用例用最简单的方式实现它的业务逻辑
因为10以内的数是比较少这里我选择用枚举法实现
class NumberConver
{
public static function arabicNumberToRomanNumber($arabicNumber)
{
$map = [
1 => 'I',
2 => 'II',
3 => 'III',
4 => 'IV',
5 => 'V',
6 => 'VI',
7 => 'VII',
8 => 'VIII',
9 => 'IX',
];
return $map[$arabicNumber];
}
}
运行测试,返回结果如下
Time: 53 ms, Memory: 6.00 MB
OK (9 tests, 9 assertions)
此时我们的第一个TDD循环就完成了
接下来我们继续针对需求2设计测试用例
/**
* @dataProvider lessThanOnHundredProvider
* @test
*/
public function it_can_conver_arabic_to_roman_when_number_less_then_one_hundred($arabic, $roman)
{
$this->assertEquals($roman, NumberConver::arabicNumberToRomanNumber($arabic));
}
public function lessThanOnHundredProvider()
{
return [
'echo X when input 10' => [10, 'X'],
'echo XX when input 20' => [20, 'XX'],
'echo XL when input 40' => [40, 'XL'],
'echo L when input 50' => [50, 'L'],
'echo LX when input 60' => [60, 'LX'],
'echo XC when input 90' => [90, 'XC'],
'echo XCI when input 91' => [91, 'XCI'],
'echo XCIV when input 94' => [94, 'XCIV'],
'echo XCIX when input 99' => [99, 'XCIX'],
];
}
因为100内的数比较多,我们不能全部枚举,所以测试用例中挑选典型数字做测试输入
运行测试,返回结果如下
Time: 71 ms, Memory: 8.00 MB
ERRORS!
Tests: 18, Assertions: 9, Errors: 9.
针对这个测试用例我们接下来要实现对应的业务逻辑
因为之前的业务是针对10以内的可以用枚举方式实现,但100内的数字比较多,我们需要重构业务逻辑。 通过观察分析我们发现,阿拉伯数和罗马数字在个位和十位上的转换具有一定相同的规则,我们可以把 两位数拆开处理
重构后的代码如下
public static function arabicNumberToRomanNumber($arabicNumber)
{
$one = [
1 => 'I',
2 => 'II',
3 => 'III',
4 => 'IV',
5 => 'V',
6 => 'VI',
7 => 'VII',
8 => 'VIII',
9 => 'IX',
];
$two = [
10 => 'X',
20 => 'XX',
30 => 'XXX',
40 => 'XL',
50 => 'L',
60 => 'LX',
70 => 'LXX',
80 => 'LXXX',
90 => 'XC',
];
$len = strlen($arabicNumber);
$string = '';
switch ($len) {
case 1:
$string = $one[$arabicNumber];
break;
case 2:
list($b, $a) = str_split($arabicNumber);
$string = $two[$b * 10] . ($a > 0 ? $one[$a] : '');
break;
}
return $string;
}
运行测试,返回结果如下
Time: 57 ms, Memory: 6.00 MB
OK (18 tests, 18 assertions)
接下来我们继续针对需求3设计测试用例
/**
* @dataProvider lessThanOneThousandProvider
* @test
*/
public function it_can_conver_arabic_to_roman_when_number_less_then_one_thousand($arabic, $roman)
{
$this->assertEquals($roman, NumberConver::arabicNumberToRomanNumber($arabic));
}
public function lessThanOneThousandProvider()
{
return [
'echo C when input 100' => [100, 'C'],
'echo CC when input 200' => [200, 'CC'],
'echo CD when input 400' => [400, 'CD'],
'echo D when input 500' => [500, 'D'],
'echo DC when input 600' => [600, 'DC'],
'echo CM when input 900' => [900, 'CM'],
'echo CMXLI when input 941' => [941, 'CMXLI'],
'echo CMXLIV when input 944' => [944, 'CMXLIV'],
'echo CMXLIX when input 949' => [949, 'CMXLIX'],
];
}
运行测试,返回结果如下
Time: 56 ms, Memory: 8.00 MB
ERRORS!
Tests: 27, Assertions: 18, Errors: 9.
针对需求3实现业务逻辑
通过观察分析我们发现,阿拉伯数和罗马数字在个位、十位和百位上的转换具有一定相同的规则,我们可以把 三位数拆开处理
重构后的代码如下
public static function arabicNumberToRomanNumber($arabicNumber)
{
$one = [
1 => 'I',
2 => 'II',
3 => 'III',
4 => 'IV',
5 => 'V',
6 => 'VI',
7 => 'VII',
8 => 'VIII',
9 => 'IX',
];
$two = [
10 => 'X',
20 => 'XX',
30 => 'XXX',
40 => 'XL',
50 => 'L',
60 => 'LX',
70 => 'LXX',
80 => 'LXXX',
90 => 'XC',
];
$three = [
100 => 'C',
200 => 'CC',
300 => 'CCC',
400 => 'CD',
500 => 'D',
600 => 'DC',
700 => 'DCC',
800 => 'DCCC',
900 => 'CM',
];
$len = strlen($arabicNumber);
$string = '';
switch ($len) {
case 1:
$string = $one[$arabicNumber];
break;
case 2:
list($b, $a) = str_split($arabicNumber);
$string = $two[$b * 10] . ($a > 0 ? $one[$a] : '');
break;
case 3:
list($c, $b, $a) = str_split($arabicNumber);
$string = $three[$c * 100] . ($b > 0 ? $two[$b * 10] : '') . ($a > 0 ? $one[$a] : '');
break;
}
return $string;
}
运行测试,返回结果如下
Time: 56 ms, Memory: 6.00 MB
OK (27 tests, 27 assertions)
最后我们针对需求4设计测试用例如下
/**
* @dataProvider numberProvider
* @test
*/
public function it_can_conver_arabic_to_roman_when_number_less_then_3999($arabic, $roman)
{
$this->assertEquals($roman, NumberConver::arabicNumberToRomanNumber($arabic));
}
public function numberProvider()
{
return [
'echo MMMCMXLIX when input 3949' => [3949, 'MMMCMXLIX'],
];
}
运行测试,返回结果如下
Failed asserting that null matches expected 'MMMCMXLIX'.
Expected :MMMCMXLIX
Actual :null
针对需求4实现业务逻辑
重构后的代码如下
public static function arabicNumberToRomanNumber($arabicNumber)
{
$one = [
1 => 'I',
2 => 'II',
3 => 'III',
4 => 'IV',
5 => 'V',
6 => 'VI',
7 => 'VII',
8 => 'VIII',
9 => 'IX',
];
$two = [
10 => 'X',
20 => 'XX',
30 => 'XXX',
40 => 'XL',
50 => 'L',
60 => 'LX',
70 => 'LXX',
80 => 'LXXX',
90 => 'XC',
];
$three = [
100 => 'C',
200 => 'CC',
300 => 'CCC',
400 => 'CD',
500 => 'D',
600 => 'DC',
700 => 'DCC',
800 => 'DCCC',
900 => 'CM',
];
$four = [
1000 => 'M',
2000 => 'MM',
3000 => 'MMM'
];
$len = strlen($arabicNumber);
$string = '';
switch ($len) {
case 1:
$string = $one[$arabicNumber];
break;
case 2:
list($b, $a) = str_split($arabicNumber);
$string = $two[$b * 10] . ($a > 0 ? $one[$a] : '');
break;
case 3:
list($c, $b, $a) = str_split($arabicNumber);
$string = $three[$c * 100] . ($b > 0 ? $two[$b * 10] : '') . ($a > 0 ? $one[$a] : '');
break;
case 4:
list($d, $c, $b, $a) = str_split($arabicNumber);
$string = $four[$d * 1000] . ($c > 0 ? $three[$c * 100] : '') . ($b > 0 ? $two[$b * 10] : '') . ($a > 0 ? $one[$a] : '');
break;
}
return $string;
}
运行测试,返回结果如下
Time: 51 ms, Memory: 6.00 MB
OK (28 tests, 28 assertions)
此时我们已经通过层层分解需求,迭代实现业务的方式完成了对需求的实现,而这个过程就是TDD。 那我们实现业务的代码是不是最优的呢,当然不是,这个可以继续优化,但我们通过TDD的方式实现的产品, 无论什么时候优化,由谁来优化,只要遵循TDD就可以保障产品是可用的,这就是TDD的魅力所在。