concordion簡要
- concordion的實(shí)現(xiàn)可以理解為活文檔,自然語言,易讀,包含很多examples。文檔是html,所以構(gòu)建出可以navigable structure的文檔。 頻繁的運(yùn)行可以使文檔和系統(tǒng)功能同步。如果系統(tǒng)行為發(fā)生了變化,相應(yīng)的文檔就會(huì)fail
- concordion文檔不包含實(shí)現(xiàn)細(xì)節(jié),把需求和實(shí)現(xiàn)分開,包含列子,實(shí)現(xiàn)方式可變,方便重構(gòu)
- concordion是Junit的擴(kuò)展
- 易學(xué),基于specifications by example、
- 不需要特定的語言格式,使用自然語言,關(guān)注易讀性
- 豐富的測試報(bào)告。 HTML格式的報(bào)告可以加入超鏈接和圖片等多種
- 文檔可以當(dāng)成unit test來運(yùn)行。具備和Junit測試一樣的深度
- BA QA DEV可以一起合作寫出文檔
- 文檔可以適用于不同級別 unit, component, subsystem, system
- 活文檔可以用來定義story的驗(yàn)收條件,幫助PO來控制項(xiàng)目范圍
- 提高測試覆蓋率。不管實(shí)現(xiàn)如何,需求一定會(huì)被滿足
concordion實(shí)現(xiàn)
一個(gè)concordion規(guī)格文檔由兩部分組成
描述功能的html文件
測試代碼 (基于Junit)
兩個(gè)文件應(yīng)該在同一個(gè)文件夾下
為了兩個(gè)文檔的融合使用,html里面應(yīng)該包含concordion的相關(guān)命令。這個(gè)命令以結(jié)點(diǎn)的屬性形式出現(xiàn)
<html xmlns:concordion="http://www.concordion.org/2007/concordion">
<body>
<p concordion:assertEquals="getGreeting()">Hello World!</p>
</body>
</html>
測試代碼基于Junit,方法名需要和html中的名字一樣,使用ConcordionRunner和Junit的RunWith來讓concordion把相關(guān)的命令和類聯(lián)系起來
package example;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@RunWith(ConcordionRunner.class)
public class HelloWorldFixture {
public String getGreeting() {
return "Hello World!";
}
}
測試報(bào)告輸出默認(rèn)目錄是 system property: java.io.tmpdir.
concordion使用方法
concordion:assertEquals:驗(yàn)證執(zhí)行方法的返回值和所期待的結(jié)果一致
concordion:set:定義一個(gè)變量
<p>
The greeting for user <span concordion:set="#firstName">Bob</span>
will be:
<span concordion:assertEquals="greetingFor(#firstName)">Hello Bob!</span>
</p>
concordion:execute:可以執(zhí)行一個(gè)測試代碼里面的方法
一般來說,執(zhí)行一個(gè)沒有返回值的方法,要用set或是setUp開頭。
<p>
If the time is
<span concordion:set="#time">09:00AM</span>
<span concordion:execute="setCurrentTime(#time)" />
then the greeting will say:
<span concordion:assertEquals="getGreeting()">Good Morning World!</span>
</p>
利用#TEXT這個(gè)特殊變量可以簡寫上面的spec??梢圆皇褂胏oncordion:set。
在把#TEXT作為參數(shù)傳給方法,后面接著#TEXT的值
<p>
If the time is
<span concordion:execute="setCurrentTime(#TEXT)">09:00AM</span>
then the greeting will say:
<span concordion:assertEquals="getGreeting()">Good Morning World!</span>
</p>
執(zhí)行一個(gè)有返回值的方法,可以使用方法接受返回值
<p>
The full name
<span concordion:execute="#result = split(#TEXT)">John Smith</span>
will be broken into first name
<span concordion:assertEquals="#result.firstName">John</span>
and last name
<span concordion:assertEquals="#result.lastName">Smith</span>.
</p>
這里的#result是一個(gè)類,split()方法返回一個(gè)類對象. firstName and lastName 是類屬性
處理不尋常的語句結(jié)構(gòu)
當(dāng)我們面對規(guī)格文檔描述不利于concordion命令的實(shí)現(xiàn)的時(shí)候,我們可以把execute命令放到更上一層結(jié)點(diǎn)結(jié)構(gòu)當(dāng)中
<p concordion:execute="#greeting = greetingFor(#firstName)">
The greeting "<span concordion:assertEquals="#greeting">Hello Bob!</span>"
should be given to user <span concordion:set="#firstName">Bob</span>
when he logs in.
</p>
execute命令的執(zhí)行是有一定的順序的
- 首先是處理所有child的set命令
- 然后是自己的命令
- 然后是自己child的execute命令
- 最后是所有child的assertEquals方法
concoirdion:execute在table里面的使用
concordion:execute on a <table> 可以設(shè)置在table heading級別來執(zhí)行table里面的每行數(shù)據(jù)
當(dāng)我們需要表現(xiàn)同一個(gè)行為的不同列子的時(shí)候,需要重復(fù)同樣的語句結(jié)構(gòu)。這種情況下可以使用table來表達(dá)
<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>
但是這樣看起來也是比較重復(fù)的
所以concordion支持把concordion命令放在table heading級別,每一個(gè)td就會(huì)重復(fù)th的命令
下面的列子中,execute在table級別,set, assertEquals在table heading級別
<table concordion:execute="#result = split(#fullName)">
<tr>
<th concordion:set="#fullName">Full Name</th>
<th concordion:assertEquals="#result.firstName">First Name</th>
<th concordion:assertEquals="#result.lastName">Last Name</th>
</tr>
<tr>
<td>John Smith</td>
<td>John</td>
<td>Smith</td>
</tr>
<tr>
<td>David Peterson</td>
<td>David</td>
<td>Peterson</td>
</tr>
</table>
concordion:execute在list上的使用
concordion:execute on a <list> 可以給方法傳遞有層級的測試數(shù)據(jù)
當(dāng)我們使用concordion在列表上的時(shí)候,比如ol 和 ul 元素, 該execute命令會(huì)執(zhí)行在每一個(gè)列表元素上。
這個(gè)特性能幫我們設(shè)置有層級結(jié)構(gòu)的測試數(shù)據(jù)
<ol concordion:execute="parseNode(#TEXT, #LEVEL)">
<li>Europe</li>
<ul>
<li>Austria</li>
<ol>
<li>Vienna</li>
</ol>
<li>UK</li>
<ul>
<li>England</li>
<li>Scotland</li>
</ul>
<li>France</li>
</ul>
<li>Australia</li>
</ol>
當(dāng)執(zhí)行到<ol concordion:execute="parseNode(#TEXT, #LEVEL)”>時(shí),給parseNode穿的參數(shù)就等于下面的表格
TEXT | #LEVEL
------- | -------
Europe | 1
Austria | 2
Vienna |3
UK | 2
England | 3
Scotland | 3
France | 2
Australia | 1
concordion:verifyRows
可以取出方法返回的可迭代的數(shù)據(jù),并一一取出做驗(yàn)證。
當(dāng)我們需要處理方法返回的大量數(shù)據(jù)內(nèi)容,并且這些數(shù)據(jù)是可迭代的時(shí)候,我們可以使用concordion:verifyRows。 比如處理一個(gè)方法返回值是列表,而我們需要對列表里面的每一個(gè)值進(jìn)行驗(yàn)證
<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>
verifyRows的語法是: var : iteration_object #loopVar : expression
expression是一個(gè)可迭代對象并且對象里面的元素對象順序是已知的
loopVar能夠訪問返回迭代對象的每一個(gè)元素,并且支持使用assertEquals去進(jìn)行驗(yàn)證
從expression返回的對象元素的順序必須與行里設(shè)置的測試數(shù)據(jù)是一致的
注解
當(dāng)我們把未完成的測試代碼加入到我們的build當(dāng)中,為了避免使build失效,我們可以加注解。
- @ExpectedToPass 期待pass
- @ExpectedToFail 期待fail
- @Unimplemented 未完成
For example:
import org.concordion.api.ExpectedToFail;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@ExpectedToFail
@RunWith(ConcordionRunner.class)
public class GreetingTest {
public String greetingFor(String firstName) {
return "TODO";
}
}
Fail-Fast
當(dāng)出現(xiàn)異常的時(shí)候,concordion會(huì)繼續(xù)執(zhí)行剩下的測試來展示所有的問題。使用Fail-Fast可以使concordion遇到第一個(gè)異常時(shí)就停止繼續(xù)執(zhí)行,跳出測試。
方法是使用注解
Fail-Fast有一個(gè)參數(shù)onExceptionType,這個(gè)參數(shù)是一個(gè)列表,列表里面的內(nèi)容是各種異常類型,只有在執(zhí)行當(dāng)中遇到列表里面的異常類型,F(xiàn)ail-Fast才會(huì)生效。
import org.concordion.api.FailFast;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@FailFast(onExceptionType={DatabaseUnavailableException.class, IOException.class})
public class MyDataTest {
public void connectToDatabase() {
....
}
}
concordion:run 可以使該文檔link到其他文檔,并且run
concordion:assertTrue 驗(yàn)證執(zhí)行方法返回值是不是True
concordion:assertFalse 驗(yàn)證執(zhí)行方法返回值是不是False
<p>
When user <b concordion:set="#firstName">Bob</b>
logs in, the greeting will be:
<b concordion:assertEquals="greetingFor(#firstName)">Hello Bob!</b>
</p>
<p>
The first name <span concordion:assertTrue="#firstName.startsWith(#letter)">starts
with <b concordion:set="#letter">B</b></span>.
</p>
<p>
The first name <span concordion:assertTrue="#firstName.startsWith(#letter)">starts
with <b concordion:set="#letter">C</b></span>.
</p>
支持Junit4.5以上, 測試代碼使用ConcordionRunner
package example;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@RunWith(ConcordionRunner.class)
public class HelloWorldFixture {
public String getGreeting() {
return "Hello World!";
}
}
返回值的類型 也可以是map
public Map split(String fullName) {
String[] words = fullName.split(" ");
Map<String, String> results = new HashMap<String, String>();
results.put("firstName", words[0]);
results.put("lastName", words[1]);
return results;
}
可以使用MultiValueResult來返回多個(gè)結(jié)果
MultiValueResult 是concordion api的一個(gè)類
package example;
import org.concordion.api.MultiValueResult;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@RunWith(ConcordionRunner.class)
public class SplittingNamesTest {
public MultiValueResult split(String fullName) {
String[] words = fullName.split(" ");
return multiValueResult()
.with("firstName", words[0])
.with("lastName", words[1]);
}
}
使用這個(gè)方法,我們可以在測試代碼里面設(shè)置比如屬性firstName的值,然后在specification里面直接調(diào)用
<span concordion:assertEquals="#result.firstName">John</span>
特殊變量 #HREF
當(dāng)我們想使用外部文件作為測試數(shù)據(jù)時(shí),可以使用#HREF指向一個(gè)link,這個(gè)link就是數(shù)據(jù)源文件。
比如我們把測試數(shù)據(jù)放在一個(gè)csv文件里面,我們就可以在html中寫一個(gè)link,然后在這個(gè)<a>中使用concordion命令和#HREF
<a href="blah.csv" concordion:execute="#x = myMethod(#HREF)">test file</a>
<span concordion:assertEquals="#x">blah.csv</span>
Write specifications, not scripts
只在Specification中描述feature和功能,不要有太詳細(xì)的步驟,具體的實(shí)現(xiàn)細(xì)節(jié)都應(yīng)該放在測試代碼里面。
Specification可以說是high level的測試腳本
concordion:echo is able to print the variable to test report
<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>
Username: <span concordion:echo="#firstName" /> # this is the variable created in specification file
</p>
</div>
<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>
Username: <span concordion:echo="username" /> # this is the class variable from test fixture code
</p>
</div>
@RunWith(ConcordionRunner.class)
public class HelloWorldFixture {
public String username = "username";
public String greetingFor(String firstName) {
return new Greeter().greetingFor(firstName);
}
}