什么是 TDD ?
TDD 有广义和狭义的区分。
广义角度指的是 ATDD(Acceptance Test Driven Development),包括 BDD(Behavior Driven Test Development)和 Consumer-Driven Contracts Development 等。
狭义角度指的是 UTDD(Unit Test Driven Development),也是我们要讲的单元测试驱动开发。
为什么需要 TDD?
传统开发周期:
测试驱动开发周期:
从上图可以看出 TDD 以测试的角度开始避免了一开始就过度设计和避免了功能不符合预期的可能。这对我们写程序来说将会充满信心,因为我们知道代码会按我们预期的方式执行。而因为有测试环节这个安全网存在,后期重构时也会更放心。
实例
需求:
给你一个整数n. 从 1 到 n 按照下面的规则打印每个数:
* 如果这个数被3整除,打印fizz.
* 如果这个数被5整除,打印buzz.
* 如果这个数能同时被3和5整除,打印fizz buzz.
* 其他情况下需要原样输出
这是一个经典的题目 FizzBuzz ,我们首先创建一个测试类:
public class FizzBuzzTests {
@Test
public void test(){
}
}
我们测试的应该是一个对象,所以我们声明一个 FizzBuzz 对象,并且他有一个 of
方法,这个方法可以接收一个数值型参数返回一个字符串结果:
public class FizzBuzzTests {
@Test
public void test(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(1);
}
}
这个时候 IDE 编译会报错,因为根本没有这个类和方法,我们先来创建类和方法:
public class FizzBuzz {
public String of(int input) {
return null;
}
}
可以看到 of
方法只是一个空实现,在之后我们将逐步实现这个功能。
需求一:其他情况下需要原样输出
测试方法:
@Test
public void testOriginalOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(1);
Assert.assertEquals("1",result);
}
我们先来运行下测试方法,发现控制台报错,错误信息如下:
java.lang.AssertionError:
Expected :1
Actual :null
这不符合我们预期的结果,接下来我们需要实现下 of
方法让它符合我们的预期,
public String of(int input) {
return String.valueOf(input);
}
再次执行测试方法,发现变绿了说明测试成功。
需求二:如果这个数被3整除,打印fizz
测试方法:
@Test
public void testFizzOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(3);
Assert.assertEquals("fizz",result);
}
运行测试方法,控制台报错:
org.junit.ComparisonFailure:
Expected :fizz
Actual :3
调整 of
方法:
public String of(int input) {
if (input % 3 == 0) {
return "fizz";
}
return String.valueOf(input);
}
执行测试方法,变绿通过。
需求三:如果这个数被5整除,打印buzz
测试方法:
@Test
public void testBuzzOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(5);
Assert.assertEquals("buzz",result);
}
运行测试方法,控制台报错:
org.junit.ComparisonFailure:
Expected :buzz
Actual :5
调整 of
方法:
public String of(int input) {
if (input % 3 == 0) {
return "fizz";
}
if (input % 5 == 0) {
return "buzz";
}
return String.valueOf(input);
}
执行测试方法,变绿通过。
需求四:如果这个数能同时被3和5整除,打印fizz buzz
测试方法:
@Test
public void testFizzBuzzOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(15);
Assert.assertEquals("fizz buzz",result);
}
运行测试方法,控制台报错:
org.junit.ComparisonFailure:
Expected :fizz buzz
Actual :fizz
调整 of
方法:
public String of(int input) {
if (input % 3 == 0) {
return "fizz";
}
if (input % 5 == 0) {
return "buzz";
}
if (input % 15 == 0) {
return "fizz buzz";
}
return String.valueOf(input);
}
我们 不加思索 的把之前的逻辑拷贝了一份,然后运行测试方法,结果发现测试不通过:
org.junit.ComparisonFailure:
Expected :fizz buzz
Actual :fizz
仔细研究了下,发现原来是判断的顺序有问题,调整后方法实现如下:
public String of(int input) {
if (input % 15 == 0) {
return "fizz buzz";
}
if (input % 3 == 0) {
return "fizz";
}
if (input % 5 == 0) {
return "buzz";
}
return String.valueOf(input);
}
执行测试方法,变绿通过。
就这样我们一步步的实现了需求并通过了测试,但是我们的工作结束了吗?
注: 在这里我们只是进行了功能性测试,一些异常情况并没有考虑进来,在实际项目中还需要更全面的测试。
从测试驱动开发周期图来看,我们还缺少了一步重构的步骤,那么什么叫重构呢?
重构有两种含义:
重构(名词):对软件内部结构的一种调整,目的是在不改变"软件之可察行为"前提下,提高其可理解性,降低其修改成本.
重构(动词):使用一系列重构准则(手法),在不改变"软件之可察行为"前提下,调整其结构
重构的两种含义都强调了不能调整软件的行为,如果有调整软件的行为的话就不能称之为重构了。
重构 FizzBuzz
我们先来看看 FizzBuzz
类的完整实现:
public class FizzBuzz {
public String of(int input) {
if (input % 15 == 0) {
return "fizz buzz";
}
if (input % 3 == 0) {
return "fizz";
}
if (input % 5 == 0) {
return "buzz";
}
return String.valueOf(input);
}
}
FizzBuzz
的实现很简单,但是我们还是发现了一些语义不清的代码,比如 input % 3 == 0
,重构后代码如下:
public class FizzBuzz {
public String of(int input) {
if (isFizzBuzz(input)) {
return "fizz buzz";
}
if (isRelatedTo(input, 3)) {
return "fizz";
}
if (isRelatedTo(input, 5)) {
return "buzz";
}
return String.valueOf(input);
}
private boolean isRelatedTo(int input, int condition) {
return input % condition == 0 || valueOf(input).contains(valueOf(condition));
}
private boolean isFizzBuzz(int input) {
return input % 15 == 0;
}
}
与之前相比,语义更加清晰了。更多重构的技巧可以查看 Martin Fowler 《重构》。
除了实现类之外,测试类也不要忘记重构,我们先来看下 FizzBuzzTests
public class FizzBuzzTests {
@Test
public void test(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(1);
}
@Test
public void testOriginalOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(1);
Assert.assertEquals("1",result);
}
@Test
public void testFizzOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(3);
Assert.assertEquals("fizz",result);
}
@Test
public void testBuzzOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(5);
Assert.assertEquals("buzz",result);
}
@Test
public void testFizzBuzzOutput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(15);
Assert.assertEquals("fizz buzz",result);
}
@Test
public void testNegativeNumberInput(){
FizzBuzz fizzBuzz = new FizzBuzz();
String result = fizzBuzz.of(-15);
Assert.assertEquals("fizz buzz",result);
}
}
我们看到
test
方法已经没有存在的意义了(在一开始告诉了我们 of 方法的行为)FizzBuzz
对象的创建在每一个方法中都存在
重构后代码:
public class FizzBuzzTests {
private FizzBuzz fizzBuzz;
@Before
public void setUp() {
fizzBuzz = new FizzBuzz();
}
@Test
public void testOriginalOutput() {
String result = fizzBuzz.of(1);
Assert.assertEquals("1", result);
}
@Test
public void testFizzOutput() {
String result = fizzBuzz.of(3);
Assert.assertEquals("fizz", result);
}
@Test
public void testBuzzOutput() {
String result = fizzBuzz.of(5);
Assert.assertEquals("buzz", result);
}
@Test
public void testFizzBuzzOutput() {
String result = fizzBuzz.of(15);
Assert.assertEquals("fizz buzz", result);
}
}
FizzBuzz
的代码可以重构的点还有很多,比如常量提取等。但是基于现在来说已经够我们使用了,后期当有调整的时候我们可以再来重构它,记住 Bob大叔提到的童子军军规 让营地比你来时更干净!