concordion学习笔记

concordion简要

  1. concordion的实现可以理解为活文档,自然语言,易读,包含很多examples。文档是html,所以构建出可以navigable structure的文档。 频繁的运行可以使文档和系统功能同步。如果系统行为发生了变化,相应的文档就会fail
  2. concordion文档不包含实现细节,把需求和实现分开,包含列子,实现方式可变,方便重构
  3. concordion是Junit的扩展
  4. 易学,基于specifications by example、
  5. 不需要特定的语言格式,使用自然语言,关注易读性
  6. 丰富的测试报告。 HTML格式的报告可以加入超链接和图片等多种
  7. 文档可以当成unit test来运行。具备和Junit测试一样的深度
  8. BA QA DEV可以一起合作写出文档
  9. 文档可以适用于不同级别 unit, component, subsystem, system
  10. 活文档可以用来定义story的验收条件,帮助PO来控制项目范围
  11. 提高测试覆盖率。不管实现如何,需求一定会被满足

concordion实现

一个concordion规格文档由两部分组成

  • 描述功能的html文件

  • 测试代码 (基于Junit)

两个文件应该在同一个文件夹下

为了两个文档的融合使用,html里面应该包含concordion的相关命令。这个命令以结点的属性形式出现

<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把相关的命令和类联系起来

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!";
    }
}

测试报告输出默认目录是 system property: java.io.tmpdir.

concordion使用方法

concordion:assertEquals:验证执行方法的返回值和所期待的结果一致

concordion:set:定义一个变量

<p>
The greeting for user <span concordion:set="#firstName">Bob</span>
will be:
<span concordion:assertEquals="greetingFor(#firstName)">Hello Bob!</span>
</p>

concordion:execute:可以执行一个测试代码里面的方法

一般来说,执行一个没有返回值的方法,要用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这个特殊变量可以简写上面的spec。可以不使用concordion:set。

在把#TEXT作为参数传给方法,后面接着#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>
执行一个有返回值的方法,可以使用方法接受返回值
        <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是一个类,split()方法返回一个类对象. firstName and lastName 是类属性

处理不寻常的语句结构

当我们面对规格文档描述不利于concordion命令的实现的时候,我们可以把execute命令放到更上一层结点结构当中

<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命令的执行是有一定的顺序的
  1. 首先是处理所有child的set命令
  2. 然后是自己的命令
  3. 然后是自己child的execute命令
  4. 最后是所有child的assertEquals方法
concoirdion:execute在table里面的使用

concordion:execute on a <table> 可以设置在table heading级别来执行table里面的每行数据

当我们需要表现同一个行为的不同列子的时候,需要重复同样的语句结构。这种情况下可以使用table来表达

            <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>

但是这样看起来也是比较重复的

所以concordion支持把concordion命令放在table heading级别,每一个td就会重复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> 可以给方法传递有层级的测试数据

当我们使用concordion在列表上的时候,比如ol 和 ul 元素, 该execute命令会执行在每一个列表元素上。
这个特性能帮我们设置有层级结构的测试数据

<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>

当执行到<ol concordion:execute="parseNode(#TEXT, #LEVEL)”>时,给parseNode穿的参数就等于下面的表格

TEXT | #LEVEL

------- | -------
Europe | 1
Austria | 2
Vienna |3
UK | 2
England | 3
Scotland | 3
France | 2
Australia | 1

concordion:verifyRows

可以取出方法返回的可迭代的数据,并一一取出做验证。

当我们需要处理方法返回的大量数据内容,并且这些数据是可迭代的时候,我们可以使用concordion:verifyRows。 比如处理一个方法返回值是列表,而我们需要对列表里面的每一个值进行验证

    <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是一个可迭代对象并且对象里面的元素对象顺序是已知的

loopVar能够访问返回迭代对象的每一个元素,并且支持使用assertEquals去进行验证

从expression返回的对象元素的顺序必须与行里设置的测试数据是一致的

注解

当我们把未完成的测试代码加入到我们的build当中,为了避免使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

当出现异常的时候,concordion会继续执行剩下的测试来展示所有的问题。使用Fail-Fast可以使concordion遇到第一个异常时就停止继续执行,跳出测试。

方法是使用注解

Fail-Fast有一个参数onExceptionType,这个参数是一个列表,列表里面的内容是各种异常类型,只有在执行当中遇到列表里面的异常类型,Fail-Fast才会生效。

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 验证执行方法返回值是不是True
concordion:assertFalse 验证执行方法返回值是不是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来返回多个结果

MultiValueResult 是concordion api的一个类

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]);
    }
}

使用这个方法,我们可以在测试代码里面设置比如属性firstName的值,然后在specification里面直接调用

<span concordion:assertEquals="#result.firstName">John</span>

特殊变量 #HREF

当我们想使用外部文件作为测试数据时,可以使用#HREF指向一个link,这个link就是数据源文件。

比如我们把测试数据放在一个csv文件里面,我们就可以在html中写一个link,然后在这个<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和功能,不要有太详细的步骤,具体的实现细节都应该放在测试代码里面。

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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,292评论 18 399
  • 转载,觉得这篇写 SQLAlchemy Core,写得非常不错。不过后续他没写SQLAlchemy ORM... ...
    非梦nj阅读 5,253评论 1 14
  • 一、源题QUESTION 1The instance abnormally terminates because ...
    猫猫_tomluo阅读 1,562评论 0 2
  • 瀑布流布局是宽度固定,高度随机,从上到下的布局方式 父元素相对定位(relative),子元素绝对定位(absol...
    嘿菠萝阅读 1,414评论 0 0