测试驱动开发的实践
最近在学习测试驱动开发,也买了本“测试驱动开发的艺术”,个人感觉获益匪浅。
TDD中的原则很简单:编码只是为了修复未通过的测试
首先从书中的一个简单的例子开始学习。
大致需求如下:需要开发一个子系统,子系统支持邮件模板功能,使用者只需要点击几下鼠标就能给员工发送个性化的邮件了。那么我们该如何用TDD开发这个系统呢?首先应该分解需求,使其变得更小,更具体。可以把模板子系统可以分解成以下测试:
1.没有任何变量的模板,渲染前后内容不变。
2.含有一个变量的模板,渲染后变量应当替换为响应的值。
3.含有多个变量的模板,渲染后变量应当替换成相应的值。
4.系统会忽略模板中不存在的变量值。
我们尝试将其转换为测试:
1.对模板“Hello,${name}”求值,当name的值为Reader时,结果应当是Hello,Reader.
2.对模板"${greeting},${name}"求值,两个变量值分别为"Hello""Reader",结果应当是Hello,Reader.
3.对模板"Hello,${name}"求值,其中变量中没有相应的值时应当抛出异常。
等等等......
下面开始我们的第一个TDD开发。现在请打开IDE,Justnow--.
写一个失败的测试:
public class TestTemplate { @Test public void oneVariable(){ Template template = new Template("Hello,${name}"); template.set("name","Reader"); String expected = "Hello,Reader"; String actual = template.execute(); assertEquals(expected, actual); } }记得在引入包的时候加上
import static org.junit.Assert.*;否则assertEquals会报语法错误。
这时候,IDE肯定会迫不及待的告诉我们Template类根本不存在,我们可以利用IDE生成相应的code.
这是我们应该会有如下代码清单:
public class Template { public Template(String string) { } public void set(String string, String string2) { } public String execute() { return null; } }
好,接下来干嘛呢?当然是运行单元测试了,这时候我们的结果肯定是失败的,因为我们根本就没有去写实现。
下一步呢?工作来了,让测试跑通!
怎么让测试通过呢?记着,我们编码的目的只是为了让测试通过,不用想太多了~不知你的实现方式是什么,试下下面这个实现方法看能否使测试跑通。
public class Template { ...//和前面一样,此处略 public String execute() { return "Hello,Reader"; } }
好,运行前面的测试,这时候我们看到,绿条出现了,说明测试通过。
当然,可以说这是投机取巧,但测试驱动开发的原则就是为了修复失败的测试。显然这种实现方式不够好,因为有硬编码的存在,所以我们需要清理代码。
下面我们加上第二条测试:
@Test public void differentTemplate(){ Template template = new Template("Hi,${name}"); template.set("name","Reader"); String expected = "Hi,Reader"; String actual = template.execute(); assertEquals(expected, actual); }
运行失败,显然,是时间修改我们的实现了。也就是说我们必须要用某种方式解析模板了。
继续使用伪实现。
首先我们要保存变量值和模板文件,也要在evaluate方法中用变量值替换模板文本中的变量,实现代码如下:
public class Template { private String templateText; private String variableValue; public Template(String templateText) { this.templateText = templateText; } public void set(String variable, String value) { this.variableValue = value; } public String execute() { return this.templateText.replaceAll("\\$\\{name\\}", variableValue); } }
这个你可能感觉是在作*弊,因为我们仍旧有硬编码,就是查找${name}的正则表达式。但这不是作*弊,我们要小步前进,记着,小步前进。
怎么消除硬编码呢?在测试中添加多个变量恐怕是最好的消除方法了吧。
好,工作又来了,消除伪实现,继续添加多变量测试。在测试类中添加如下代码:
@Test public void multipleVariables(){ Template template = new Template("${one},${two},${three}"); template.set("one","1"); template.set("two","2"); template.set("three","3"); String expected = "1,2,3"; String actual = template.execute(); assertEquals(expected, actual); }
测试,运行,失败(如果不失败反而不正常了;-))
现在我们可以使用查找替换的方法实现功能,代码清单如下:
import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; public class Template { private Map<String, String> variables; //store the variables. private String templateText; public Template(String templateText) { variables = new HashMap<String, String>(); this.templateText = templateText; } public void set(String variable, String value) { this.variables.put(variable, value); } public String execute() { String result = templateText; for(Entry<String,String> entry:variables.entrySet()){ // iterator the variables. String regex = "\\$\\{" + entry.getKey() + "\\}"; result = result.replaceAll(regex, entry.getValue()); } return result; } }
下面运行我们的测试代码,全部绿条,通过。
接下来我们测试一下如果输入模板中不存在的变量会是什么效果,添加测试用例:
@Test public void unknownVariableAreIgnored(){ Template template = new Template("Hi,${name}"); template.set("name","Reader"); template.set("do not exist","Hi"); //this variable is not exist in the template. String expected = "Hi,Reader"; String actual = template.execute(); assertEquals(expected, actual); }
我猜这个是能够通过测试的,因为我们的业务代码已经很强大了,运行,的确全部通过运行了。
下面,是时间重构了,因为测试代码和产品代码同等重要,我们看下我们的测试代码清单:
public class TestTemplate { @Test public void oneVariable(){ Template template = new Template("Hello,${name}"); template.set("name","Reader"); String expected = "Hello,Reader"; String actual = template.execute(); assertEquals(expected, actual); } @Test public void differentTemplate(){ Template template = new Template("Hi,${name}"); template.set("name","Reader"); String expected = "Hi,Reader"; String actual = template.execute(); assertEquals(expected, actual); } @Test public void multipleVariables(){ Template template = new Template("${one},${two},${three}"); template.set("one","1"); template.set("two","2"); template.set("three","3"); String expected = "1,2,3"; String actual = template.execute(); assertEquals(expected, actual); } @Test public void unknownVariableAreIgnored(){ Template template = new Template("Hi,${name}"); template.set("name","Reader"); template.set("do not exist","Hi"); //this variable is not exist in the template. String expected = "Hi,Reader"; String actual = template.execute(); assertEquals(expected, actual); } }
我们会发现里面有很多重复冗余的代码等待我们去清理了,首先所有的测试都使用了Template对象,所以我们最好将其提取为成员变量,其次所有的测试方法都把evaluate方法的返回值作为被比较对象进行比较,最好能够消除这种重复。同时我们也应该回头检视我们的测试代码,消除重复的测试,比如第一个,第二个和最后一个就是重复的,另外我们还可以让unknownVariableAreIgnored()使用multipleVariables()中的模板文件。
所以,消除冗余测试并统一风格后的测试代码如下:
public class TestTemplate { private Template template; @Before public void setUp(){ template = new Template("${one},${two},${three}"); template.set("one","1"); template.set("two","2"); template.set("three","3"); } @Test public void multipleVariables(){ String expected = "1,2,3"; assertTemplateEvaluatesTo(expected); } @Test public void unknownVariableAreIgnored(){ template.set("do not exist","Hi"); //this variable is not exist in the template. String expected = "1,2,3"; assertTemplateEvaluatesTo(expected); } private void assertTemplateEvaluatesTo(String expected){ assertEquals(expected,template.evaluate()); } }
现在的测试代码看着是不是更加简练了呢?这样测试代码本身更轻快短小,仅仅关注要测试的业务逻辑。
接下来,我们现在该继续写测试,添加新功能了。目前我们的模板引擎已经有了基本的功能,下一步应该考虑添加错误处理功能了。不过步骤依然如此,循环渐进,一步一步来,记着,小步前进。当我们继续完善功能的时候,我们会发现我们的evaluate()方法会越来越臃肿,这时候又到了重构的时候了,但由于我们有单元测试做保证,只要保持我们的绿灯常亮,就可以放心的去重构。