欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

测试驱动开发的实践

程序员文章站 2022-04-07 12:27:32
...
最近在学习测试驱动开发,也买了本“测试驱动开发的艺术”,个人感觉获益匪浅。
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, Just now - -.
写一个失败的测试:
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()方法会越来越臃肿,这时候又到了重构的时候了,但由于我们有单元测试做保证,只要保持我们的绿灯常亮,就可以放心的去重构。