TDD实践初体验-敏捷开发训练营系列

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之间
  • 输出值是转换后的罗马数字

我们可以把先把这个需求分解成多个小需求,比如我们可以把这个需求分解成如下小需求

  1. 处理10以内一位数的情况
  2. 处理100以内两位数的情况
  3. 处理1000以内三位数的情况
  4. 处理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的魅力所在。