引言
如果要咱们回答在软件开发过程中最怕的是啥?可能很多人都会回答改老代码,也可能会有不少人回答改需求,那有没有想过为什么会害怕改老代码,为什么会害怕改需求?答案很简单,怕改坏了。不敢碰老代码,怕一改这里,那里就会莫名其妙的坏了;不喜欢需求变来变去,怕需求一变,之前的设计就不管用了,很多的代码结构都可能变化,而代码一变就怕出问题。如果再深思一下,其实我们怕的核心原因在于没有一个快速有效的检查机制,没有代码测试,让我马上就能知道我是不是把哪里改的有问题,我的改动是不是引入了新的问题,这就是怕的根源。
TDD登场
对于上面的问题有解决方案吗?当然有,很简单啊,没有检查机制就加上检查机制啊,我们加上完善的代码测试就行啦。这就到本文的主题了,TDD(Test Driven Development),测试驱动开发。就是咱们不仅要写测试来建立检查机制,还要最先写测试,让测试先行,然后再开发功能。可能有人第一次听这种开发方式会非常疑惑,好像很反逻辑,先写测试怎么能行,功能都还没有呢,测个啥呢!
TDD来源
其实呢,TDD的思想可以说是来自于极限编程(Extreme programming,简称XP),很多人不理解极限编程是啥,因为这个中文翻译是在是有点烂,这里简单的介绍下极限编程。我的理解其实极限编程很简单,它的核心思想一句话来说就是 “如果我们认为一个实践是好的,那就把它做到极致”。比如说,我们认为写测试是好的实践,那我们就做到极致,先写测试再开发,这就有了TDD;我们认为集成部署是好的实践,那我们就做到极致,持续集成持续部署,这就有了CICD。个人认为这就是TDD开发方式的思想由来。
TDD原理
因为TDD是一个比较宽泛的词汇,不同的人可能有不同的理解,这里特别说明一下的是, 文章涉及到 TDD 专指 UTDD(Unit Test Driven Development)。
铺垫了这么多,那下面就来看看TDD到底是怎么“玩”的。TDD其实就三个步骤,测试不可运行,测试可运行,代码重构。来解释一下:
1. 测试不可运行 :写一个功能最小完备的单元测试,并且该单元测试编译是不能通过的。
2. 测试可运行 :快速编写刚刚好使测试通过的代码,不需要多写,够用就行。
3. 代码重构 :消除刚刚编码过程引入的重复设计,优化设计结构,当然重构并不专指对功能代码,如果测试代码有重复和不合理的地方,也是一样需要重构的。
然后不停的重复这三个步骤。这就是TDD的原理,很简单,对吧!
TDD实战
下面来点具体的实践,实战一下TDD的三个步骤。
假设现在需要实现的功能是接收一串字符串参数,经过处理后,输出:“Hello 字符串”;如果输入参数为空字符串,就输出 “Hello world”。如果用TDD的方式来开发的话,我们先思考一下这个功能,可以分为两种情况,一是有input,二是input为空,所以咱们可能需要两个测试。(这里用java来实现)
咱们来先写第一个测试:
@Test
public void notEmptyInput() {
String input = "mockInput";
String output = new OutputService().getOutput(input);
assertEquals("Hello mockInput", output);
}
好,现在这个一定是无法测试通过的,我们甚至都没有一个OutputService, 更别谈调用它的getOutput方法了。现在需要让这个测试通过,我们来写一个刚好够用的实现。
public class OutputService {
public String getOutput(String input) {
return "Hello" + " " + input;
}
}
现在运行这个测试,应该是能通过的。目前代码足够简洁,我们不需要考虑重构,我们在来考虑写下一个测试,目前空字符串的情况我们还没有考虑到,我们来写一个字符串为空的测试。
@Test
public void emptyInput() {
String input = "";
String output = new OutputService().getOutput(input);
assertEquals("Hello world", output);
}
现在这个测试肯定是无法通过运行的,现在我们需要修改实现来让我们的第二个测试通过。
public class OutputService {
public String getOutput(String input) {
if("".equals(input)) {
return "Hello world";
} else {
return "Hello" + " " + input;
}
}
}
好,现在第二个测试应该就能通过了。但是我们看看我们功能实现的代码,if else看着是不是有点扎眼,处理input的方式也没能抽出来,闻到了代码的bad smell. 下面我们就来进行一个简单的重构。
public class OutputService {
public String getOutput(String input) {
if("".equals(input)) {
input = "world";
}
return handleInput(input);
}
private String handleInput(String input) {
return "Hello " + input;
}
}
代码重构玩之后,咱们运行一下两个测试,看看改动有没有造成问题。测试通过,代表咱们这个小小的重构就完成啦。其实测试代码也可以重构的,测试中的new OutputService()也是比较重复的代码,咱们可以利用spring框架的自动注入来完成,本文主要讲解的是TDD,这里就不多说重构了。通过这个例子,相信大家能大概理解TDD的工作方式了。
质疑
那可能有人会抛出这么一个疑问,如果我遵循TDD的开发方式,那我啥时候做设计呢,我们之前的开发方法,可是先要做完善的设计,然后再遵循设计开发的啊。
其实我们在实战的例子中可以看到,我们在写第一个测试的时候,就在测试中写出了OutputService和getOutput方法,这其实就是设计的体现,我们已经预先设想了实现的方式了。我们会在写测试的时候,先设想系统的设计或者功能的设计,然后在重构的时候,可能会修改之前的设计。所以TDD肯定是有设计的。
个人觉得这是一个设计的理念的问题,瀑布式的开发方式,会在写代码之前做完善详尽的系统和功能设计,并且相信当前的设计就是最好的设计,代码开发只用照着设计的方式去写就好了。而TDD是将设计的过程分散开来了,我们会在前期做一些设计,但可能不会特别详尽和完整,只是一个大概的版本,因为我们相信需求可能随时会变化,设计也一定会演进的,然后这个设计版本会在草稿纸上经过几个迭代,让我们觉得这个设计目前是OK的,是能满足功能的,然后我们再用TDD去开发,在TDD的过程中再去完善我们的设计,通过重构的方法,通过我们对设计模式的理解和我们编程的经验,在coding的过程中慢慢来找到最好的设计。
总结
本文讲了TDD的概念,来源,原理以及实例,相信整体来说并不难理解。其实TDD最难的是坚持,坚持测试先行,坚持重构,坚持改善设计,这些都不是复杂的概念,但知易行难。只有在实践中不断的总结和思考,才能真正的掌握这项方法。本人也在慢慢学习,文中如果有错误和不当之处,请大家帮忙指出来,最后希望路上有更多的同行者。
参考文献
《代码整洁之道》
《测试驱动开发的艺术》