问题域?
测试及文档,Specification By Example 应该怎么做,怎样提高测试的效率。
什么是Concordion ?
Concordion是一种自动化测试框架,是测试及文档,Specification By Example的一种实现,可以用自然的语言来描述软件功能,能很好的描述用户的Story。它不仅支持Java,也同样支持.NET, Python, Scala, and Ruby. 是JUnit的扩展,写好的测试不仅是格式良好的测试文档,同样也可以当成JUnit单元测试来跑。
如何使用 ?
Concordiong很简单,我们可以直接看看官方的例子。
1. 安装
下载例子concordion-kickstart, 例子使用Gradle构建工具管理的,比maven简单,这里就不详细介绍它的使用方式了。
Greeter.java 需要测试的类。
specs 验收测试归类文件夹。
concordion.css 级联样式表让测试展现更好看一点。
HelloWordTest.java JUnit测试类,集成至ConcordionTestCase
HelloWord.html Concordion测试文档,名字必须与测试类保持一致。
环境要求Jdk1.5以上,依赖一下jar包:
- concordion-1.4.2.jar
- junit-3.8.2.jar or junit-4.8.2.jar
- ognl-2.6.9.jar
- xom-1.2.5.jar
2. 导入项目到IDE中,Eclipse或者IntelliJ IDEA
使用Gradle很方便导入,在gradle的构建脚本build.gradle最前面加上或者intellij插件即可,如下:
然后在命令行运行 gradle eclipse或者gradle idea,然后通过IDE导入就可以了。
3. Concordion测试
Concordion测试文档需要遵循一些固定的格式,它必须是一个格式良好的XHTML文档,所以必须包含
<html xmlns:concordion="http://www.concordion.org/2007/concordion">
concordion:assertEquals用来断言结果的正确性。
concordion:set设置变量值。
concordion:execute执行测试类方法。
看第一个简单的例子:
JUnit测试类:
package com.example.specs.greeting;
import org.concordion.integration.junit3.ConcordionTestCase;
import com.example.Greeter;
public class HelloWorldTest extends ConcordionTestCase {
public String greetingFor(String firstName) {
return new Greeter().greetingFor(firstName);
}
public void setCurrentTime(String time) {
// TODO
}
}
测试文档:
<html xmlns:concordion="http://www.concordion.org/2007/concordion">
<link href="../concordion.css" rel="stylesheet" type="text/css" />
<body>
<h1>Hello World!</h1>
<p>
After a user logs into the system, a greeting is
displayed saying "Hello [user's first name]!"
</p>
<div class="example">
<h3>Example</h3>
<p>
When user <b concordion:set="#firstName">World</b>
logs in, the greeting will be:
<b concordion:assertEquals="greetingFor(#firstName)">Hello World!</b>
</p>
<p>
If time is
<span concordion:execute="setCurrentTime(#TEXT)">09:00AM</span>
</p>
</div>
</body>
</html>
第一个测试很容易理解greetingFor函数接受一个字符串并返回,通过concordion:set设置firstName为World,用concordion:assertEquals判断最后结果是否是Hello World.输出界面为:
第二个例子:测试函数返回值
package com.example.specs.greeting;
import org.concordion.integration.junit3.ConcordionTestCase;
public class SplittingNamesTest extends ConcordionTestCase{
public Result split(String fullName){
String[] words = fullName.split(" ");
Result result = new Result(words[0], words[1]);
return result;
}
class Result {
private final String firstName;
private final String lastName;
Result(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName(){
return firstName;
}
public String getLastName(){
return lastName;
}
}
}
<html xmlns:concordion="http://www.concordion.org/2007/concordion">
<head>
<link href="../concordion.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<h1>Splitting Names</h1>
<p>
To help personalise our mailshots we want to have the first name
and last name of the customer. Unfortunately the customer data
that we are supplied only contains full names.
</p>
<p>
The system therefore attempts to break a supplied full name into
its constituents by splitting around whitespace.
</p>
<div class="example">
<h3>Example</h3>
<table>
<tr>
<th>Full Name</th>
<th>First Name</th>
<th>Last Name</th>
</tr>
<tr concordion:execute="#result=split(#fullName)">
<td concordion:set="#fullName">John Smith</td>
<td concordion:assertEquals="#result.firstName">John</td>
<td concordion:assertEquals="#result.lastName">Smith</td>
</tr>
<tr concordion:execute="#result=split(#fullName)">
<td concordion:set="#fullName">David Peterson</td>
<td concordion:assertEquals="#result.firstName">David</td>
<td concordion:assertEquals="#result.lastName">Peterson</td>
</tr>
</table>
</div>
</body>
</html>
使用concordion:execute调用函数split,返回结果保存在result变量里面,Concordion会自动把它映射为Result对象。
第三个例子:测试数据集
package com.example.specs.greeting;
import org.concordion.integration.junit3.ConcordionTestCase;
import java.util.*;
public class PartialMatchesTest extends ConcordionTestCase {
private Set<String> usernamesInSystem = new HashSet<String>();
public void setUpUser(String username){
usernamesInSystem.add(username);
}
public Iterable<String> getSearchResultsFor(String searchString){
SortedSet<String> matches = new TreeSet<String>();
for(String username : usernamesInSystem){
if(username.contains(searchString)){
matches.add(username);
}
}
return matches;
}
}
<html xmlns:concordion="http://www.concordion.org/2007/concordion">
<head>
<link href="../concordion.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<h1>Partial Matches</h1>
<p>
Username searches return partial matches, i.e. all usernames containing
the search string are returned.
</p>
<div class="example">
<h3>Example</h3>
<p>Given these users:</p>
<table concordion:execute="setUpUser(#username)">
<tr><th concordion:set="#username">Username</th></tr>
<tr><td>john.lennon</td></tr>
<tr><td>ringo.starr</td></tr>
<tr><td>george.harrison</td></tr>
<tr><td>paul.mccartney</td></tr>
</table>
<p>Searching for "<b concordion:set="#searchString">arr</b>" will return:</p>
<table concordion:verifyRows="#username:getSearchResultsFor(#searchString)">
<tr><th concordion:assertEquals="#username">Matching Usernames</th></tr>
<tr><td>george.harrison</td></tr>
<tr><td>ringo.starr</td></tr>
</table>
</div>
</body>
</html>
使用concordion:verifyRows来接受结果集,并判断结果集中的数据,相当简单一看就懂。
结束语
通过上面的例子看Concordion还是比较简单的,它鼓励把测试写成类似用户Story描述那样,让项目中的每个人都能看懂测的具体是什么场景包括BA或者其他不懂技术的人。它是JUnit的扩展,能像JUnit测试一样自动话执行和生成结果。比起其他的BDD工具JBehave, Cucumber有些区别的就是它并不使用Given When Then的格式,它提倡自然语言Specification By Example的方式来描述测试用例。