测试驱动开发的实践

最近在学习测试驱动开发,也买了本“测试驱动开发的艺术”,个人感觉获益匪浅。

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()方法会越来越臃肿,这时候又到了重构的时候了,但由于我们有单元测试做保证,只要保持我们的绿灯常亮,就可以放心的去重构。

相关推荐