本文主要理解TDD的概念以及原则,同时能够用自己的方式实施TDD。
Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: requirements are turned into very specific test cases, then the software is improved to pass the new tests, only. This is opposed to software development that allows software to be added that is not proven to meet requirements.
从上面的定义可以看出,TDD是软件开发的一种方式。很久以前,我们开发软件的过程可能是这个样子的:先接到需求,然后蒙头写业务代码去实现需求,最后去做手动或写自动测试来看是否满足了需求。而TDD的开发模式是:将先接到的需求,转换为对应的测试(UT),然后写业务代码去通过这些测试,一旦通过了所有测试,就表示业务需求被实现了。所以,TDD是测试驱动了开发,而以前的开发方式是业务驱动了开发。
从上面的TDD流程图可以看出,TDD主要经历一下几步:
- 根据需求,先写一个测试,运行这个测试,发现这个测试是失败的(图中红色部分)。
- 为了通过这个测试,写最少的业务代码,然后运行这个测试,通过测试(图中绿色部分)。
- 重构代码,提高代码的可读性,维护性等,运行所有测试并保证其通过(图中紫色部分)。
- 再根据需求写一个测试,重复上面的三步。
- 直到所有的业务都转换成测试,所有的测试都通过。
要回答为什么需要TDD,就要看看TDD具有什么价值了:
- TDD属于单元测试,具有单元测试测试的所有价值,包括保证代码质量,为后期重构提供便利,为开发提供说明文档等。
- TDD是测试驱动的开发,这个过程能够让开发在面对复杂业务逻辑的时候,进行合理的拆分,有利于开发对业务逻辑的把控。
- 在实施TDD的过程中,每通过一个测试,能够让开发对业务实现更加自信,提升工作效率。
要做好TDD,必须要满足最基本的三条原则:
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
上面的三原则看起来比较拗口,我给大家翻译一下:
-
除非这能让失败的单元测试通过,否则不允许去编写任何的产品代码。
-
只允许编写刚好能够导致失败的单元测试。 (编译失败也属于一种失败)
-
只允许编写刚好能够导致一个失败的单元测试通过的产品代码。
三原则理解起来很抽象,下面我们通过一个实例来让大家感受一下:
下面我们举个例子来用TDD的方式一步一步实现:
例子: 假设我们有一个计算器,能够接受两个整数数字和具体操作,下面是相应的操作以及返回结果:
- ADD:返回两个整数数字之和。例如 1 + 2 = 3;
- SUB:返回两个整数数字的差,计算方式为第一个数字减去第二个数字。例如:2 -1 = 1;
- MULTIPLY:返回两个整数数字的乘积。例如:2 * 3 = 6;
- DIVISION:返回两个整数数字的商,计算方式为第一个数除以第二个数字,并且除数不能为0。例如:6 ÷ 2 = 3;
代码实现:
-
计算两个整数之和:
先写一个测试:
public class CalculatorTest { @Test public void test_add_when_1_and_2_return_3(){ int result = Calculator.calculate(1, 2, OperationMethod.ADD); assertEquals(3, result); } }
我们运行这个测试,发现报错,报错的原因有两个,没有Calculator类和OperationMethod的枚举,于是我们创建这个类和枚举, 并在里面创建方法和枚举属性:
public class Calculator { public static int calculate(int number1, int number2, OperationMethod method) { return number1 + number2; } }
public enum OperationMethod { ADD }
运行测试,发现可以通过。
-
计算两个数字之差:
我们在CalculatorTest里面添加另一个测试:
@Test public void test_sub_when_2_sub_1_return_1(){ int result = Calculator.calculate(2, 1, OperationMethod.SUB); assertEquals(1, result); }
运行这个测试,报错,发现枚举OperationMethod里面没有SUB属性,添加上去:
public enum OperationMethod { SUB, ADD }在运行这个测试,报错,发现方法calculate的实现有问题,于是修改如下:
public static int calculate(int number1, int number2, OperationMethod method) { switch (method) { case ADD: return number1 + number2; case SUB: return number1 - number2; default: return number1 + number2; } }
运行这个测试,发现可以通过,然后运行这个测试,也可以通过。
-
计算两个数字的乘积:
我们在CalculatorTest里面添加另一个测试:
@Test public void test_multiply_when_2_multiply_3_return_6(){ int result = Calculator.calculate(2, 3, OperationMethod.MULTIPLY); assertEquals(6, result); }
运行这个测试,报错,在OperationMethod里面添加属性MULTIPLY上去:
public enum OperationMethod { SUB, MULTIPLY, ADD }
再运行这个测试,发现还是报错,修改calculate的逻辑:
public static int calculate(int number1, int number2, OperationMethod method) { switch (method) { case ADD: return number1 + number2; case SUB: return number1 - number2; case MULTIPLY: return number1 * number2; default: return number1 + number2; } }
运行测试,发现通过,运行所有测试,也都可以通过。
-
计算两个数的商:
先写一个测试:
@Test public void test_division_when_6_division_2_return_3(){ int result = Calculator.calculate(6, 2, OperationMethod.DIVISON); assertEquals(3, result); }
运行这个测试,发现这个测试没有通过,然后再OperationMethod里面添加属性DIVISON:
public enum OperationMethod { SUB, MULTIPLY, DIVISON, ADD }
运行这个测试,发现报错,然后修改calculte的逻辑:
public static int calculate(int number1, int number2, OperationMethod method) { switch (method) { case ADD: return number1 + number2; case SUB: return number1 - number2; case MULTIPLY: return number1 * number2; case DIVISON: return number1 / number2; default: return number1 + number2; } }
运行这个测试,发现可以通过,然后运行所有测试,依旧可以通过。
之后我们写一个除数为0的测试:
@Test(expected = DivisorZoreException.class) public void test_division_when_division_0_throw_exception(){ Calculator.calculate(6, 0, OperationMethod.DIVISON); }
运行测试,发现报错,添加如下异常:
public class DivisorZoreException extends RuntimeException { }
运行测试,发现报错,修改calculate的逻辑:
public static int calculate(int number1, int number2, OperationMethod method) { switch (method) { case ADD: return number1 + number2; case SUB: return number1 - number2; case MULTIPLY: return number1 * number2; case DIVISON: if (number2 == 0) { throw new DivisorZoreException(); } return number1 / number2; default: return number1 + number2; } }
运行测试,全部通过。
-
完成所有功能。
用TDD的方式实现下面的用户注册逻辑:
- 用户需要输入用户名,密码和确认密码才能注册。
- 用户名和密码不能为空,如果是空,给予用户提示。
- 用户名必须是邮箱,如果不是邮箱,给予用户提示。
- 密码必须是数组加字母的组合,长度不能小于8位,并且必须要带一个特殊字符,如果不符合要求,给予用户提示。
- 确认密码和密码必须完全一致,如果不一致,需要给予用户提示。
- 当用户注册成功,提示用户:恭喜您注册成功!

