摘要:在验收测试框架Fitneese中,使用Scenario可以把最常用的测试步骤封装起来,从而达到模块化定义Fitnesse测试用例的能力。但Scenario仅限于封装Script测试步骤,Script实例要先创建,然后才能调用;Scenario也不能封装Table。本文后半部分展示修改Fitneese代码,扩展Scenario的封装范围。

首先普及一下概念,什么是Fitnesse,听一听.NET版Cucumber的创始人Aslak Hellesøy谈Fitnesse与Cucumber对比:

FIT/Fitnesse和Cucumber都执行高级语言编写的验收测试。FIT仅识别HTML,Fitnesse则通过提供Wiki语法来简化编写测试的过程。在FIT/Fitnesse当中,所有的测试都以表格的形式呈现。

FitNesse比Cucumber的优势在于Wiki支持。

原文链接:http://www.infoq.com/cn/news/2009/11/interview-cucumber-for-dotnet

1.Scenario是什么

Fitneese的SliM UserGuide中介绍了 Scenario

原文是这么介绍Scenario的:

A Scenario table is a table that can be called from other tables; namely Script Table and Decision Table.

The format of a Scenario table is the same as the format of a Script Table, but with a few differences. You can see a Scenario table in action here.

Scenario是一种Table,可以被Script Table 和 Decision Table调用。

由此很多人都对Scenario报了很大的期望,希望能用Scenario模块化封装测试步骤。

2.Scenario能力展示

下面是我结合Script示例和Scenario示例写的一个Scenario演示用例:

wiki文本:

!define TEST_SYSTEM {slim}
!path classes |import|
|fitnesse.slim.test| !4 定义scenario checkLogin: 登录并检查结果
| scenario | checkLogin | u || p || ensure || logged |
| @{ensure} | login with username | @{u} | and password | @{p} |
| check @{logged} | login message | @{u} logged in. |
| show | number of login attempts | !4 创建script实例,后面调用scenario都是针对这个实例
| script | login dialog driver | Bob | xyzzy | !4 Invoking a scenario from a !-DecisionTable-!
| checkLogin |
| u | p | ensure | logged |
| Bob | xyzzy | ensure | |
| Bob | zzyxx | reject | not |
| Cat | xyzzy | reject | not | !4 Invoking a scenario from a !-ScriptTable-!
| script |
| checkLogin | Bob || zzyxx || reject || not |
| checkLogin | Bob || xyzzy || ensure || | !4 script原示例
| script | login dialog driver | Bob | xyzzy |
| login with username | Bob | and password | xyzzy |
| check | login message | Bob logged in. |
| reject | login with username | Bob | and password | bad password |
| check | login message | Bob not logged in. |
| check not | login message | Bob logged in. |
| ensure | login with username | Bob | and password | xyzzy |
| note | this is a comment |
| show | number of login attempts |
| $symbol= | login message | The fixture for this table is:{{{public class LoginDialogDriver {
private String userName;
private String password;
private String message;
private int loginAttempts; public LoginDialogDriver(String userName, String password) {
this.userName = userName;
this.password = password;
} public boolean loginWithUsernameAndPassword(String userName, String password) {
loginAttempts++;
boolean result = this.userName.equals(userName) && this.password.equals(password);
if (result)
message = String.format("%s logged in.", this.userName);
else
message = String.format("%s not logged in.", this.userName);
return result;
} public String loginMessage() {
return message;
} public int numberOfLoginAttempts() {
return loginAttempts;
}
} }}}

测试用例页面:

点击Test执行后:

展开DecisionTable调用Scenario的测试结果:

展开ScriptTable调用Scenario的测试结果:

至此,我们看到Scenario可以把Script步骤封装起来,取个模块名,然后使用DecisionTable或ScriptTable调用。

3.Scenario的局限

请注意调用Scenario前的这一行:

目的是在调用Scenario前先创建好Script实例。

如果去掉这一句,再执行,是这样的结果:

再尝试一下,把创建Script实例的语句塞到Scenario中:

!4 定义scenario checkLogin: 登录并检查结果
| scenario | checkLogin | u || p || ensure || logged |
| script | login dialog driver | Bob | xyzzy | <--这是新加的创建Script实例的语句
| @{ensure} | login with username | @{u} | and password | @{p} |
| check @{logged} | login message | @{u} logged in. |
| show | number of login attempts |

保存后执行测试:

4.不满意怎么办?

我还想使用Scenario封装TableTable,比如RestFixture定义的TableTable,

国外最著名的软件开发问答网站stackoverflow.com也在问:

Can I make a scenario of RestFixture table in fitnesse?, or is there another way to make reusable components?

我准备修改Fitneese代码,使得Scenario能直接封装ScriptTable和TableTable,请往下看……

5.修改ScenarioTable.java,使Scenario能直接封装ScriptTable

Scenario的源代码在目录D:\git\FitnesseKit\fitnesse\src\fitnesse\testsystems\slim\tables下:

打开ScenarioTable.java后,关键代码是Scenario的参数@xxx是怎么替换的:

      @Override
public String substitute(String content) throws SyntaxError {
for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {
String arg = scenarioArgument.getKey();
if (getInputs().contains(arg)) {
String argument = scenarioArguments.get(arg);
content = StringUtil.replaceAll(content, "@" + arg, argument);
content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);
} else {
throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
}
}
return content;
}
});

增加两行打印System.out.println:

      @Override
public String substitute(String content) throws SyntaxError {
+ System.out.println("ScenarioTable.call.substitute <<<<<<<<<< content:" + content);
for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {
String arg = scenarioArgument.getKey();
if (getInputs().contains(arg)) {
String argument = scenarioArguments.get(arg);
content = StringUtil.replaceAll(content, "@" + arg, argument);
content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);
} else {
throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
}
}
+ System.out.println("ScenarioTable.call.substitute >>>>>>>>>> content:" + content);
return content;
}

在D:\git\FitnesseKit\fitnesse\src\fitnesse\testsystems\slim\tables\SlimTable.java的构造函数SlimTable中增加一行打印:

      public SlimTable(Table table, String id, SlimTestContext testContext) {
+ System.out.println("SlimTable.SlimTable table:"+table);
this.id = id;
this.table = table;
this.testContext = testContext;
tableName = getTableType() + "_" + id;
}

目的是查看每次启动的测试Table,比如一次import,一次ScriptTable,一次DecisionTable,一次TableTable,等等。

使用命令ant compile重新编译Fitnesse,并输入ant run重新启动Fitneese:

D:\git\FitnesseKit\fitnesse>ant compile

...

D:\git\FitnesseKit\fitnesse>ant run

再次运行刚刚失败的测试,现在看命令行打印:

 [java] ScenarioTable.call.substitute <<<<<<<<<< content:<table>
[java] <tr>
[java] <td>scenario</td>
[java] <td>checkLogin</td>
[java] <td>u</td>
[java] <td></td>
[java] <td>p</td>
[java] <td></td>
[java] <td>ensure</td>
[java] <td></td>
[java] <td>logged</td>
[java] </tr>
[java] <tr>
[java] <td>Script</td>
[java] <td>login dialog driver</td>
[java] <td>Bob</td>
[java] <td colspan="6">xyzzy</td>
[java] </tr>
[java] <tr>
[java] <td>@{ensure}</td>
[java] <td>login with username</td>
[java] <td>@{u}</td>
[java] <td>and password</td>
[java] <td colspan="5">@{p}</td>
[java] </tr>
[java] <tr>
[java] <td>check @{logged}</td>
[java] <td>login message</td>
[java] <td colspan="7">@{u} logged in.</td>
[java] </tr>
[java] <tr>
[java] <td>show</td>
[java] <td colspan="8">number of login attempts</td>
[java] </tr>
[java] </table>
[java] ScenarioTable.call.substitute >>>>>>>>>> content:<table>
[java] <tr>
[java] <td>scenario</td>
[java] <td>checkLogin</td>
[java] <td>u</td>
[java] <td></td>
[java] <td>p</td>
[java] <td></td>
[java] <td>ensure</td>
[java] <td></td>
[java] <td>logged</td>
[java] </tr>
[java] <tr>
[java] <td>Script</td>
[java] <td>login dialog driver</td>
[java] <td>Bob</td>
[java] <td colspan="6">xyzzy</td>
[java] </tr>
[java] <tr>
[java] <td>ensure</td>
[java] <td>login with username</td>
[java] <td>Bob</td>
[java] <td>and password</td>
[java] <td colspan="5">xyzzy</td>
[java] </tr>
[java] <tr>
[java] <td>check </td>
[java] <td>login message</td>
[java] <td colspan="7">Bob logged in.</td>
[java] </tr>
[java] <tr>
[java] <td>show</td>
[java] <td colspan="8">number of login attempts</td>
[java] </tr>
[java] </table>
[java] SlimTable.SlimTable table:[[scenario,checkLogin,u,,p,,ensure,,logged],[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]

再去运行一个没有被Scenario的封装的Script:

| Script | login dialog driver | Bob | xyzzy |
| ensure | login with username | Bob | and password | xyzzy |
| check | login message | Bob logged in. |
| show | number of login attempts |

命令行打印如下内容:

 [java] SlimTable.SlimTable table:[[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]

对比一下两种运行的打印:

[java] SlimTable.SlimTable table:[[scenario,checkLogin,u,,p,,ensure,,logged],[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]


[java] SlimTable.SlimTable table:[[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]

只要想办法在运行封装时,去掉[scenario,checkLogin,u,,p,,ensure,,logged],,说不定就可以了。

接下去,修改substitute函数:

      public String substitute(String content) throws SyntaxError {
System.out.println("ScenarioTable.call.substitute <<<<<<<<<< content:" + content);
+ int trLeftFirstIndex = content.indexOf("<tr>");
+ int trRightFirstIndex = content.indexOf("</tr>");
+ int trLeftSecondIndex = content.indexOf("<tr>", trLeftFirstIndex + 1);
+ int trRightSecondIndex = content.indexOf("</tr>", trRightFirstIndex + 1);
+ int scriptIndex = content.toLowerCase().indexOf("<td>script</td>");
+ if(scriptIndex > trLeftSecondIndex && scriptIndex < trRightSecondIndex) {
+ StringBuffer removeFirstTr = new StringBuffer();
+ removeFirstTr.append(content.substring(0, trLeftFirstIndex));
+ removeFirstTr.append(content.substring(trRightFirstIndex + "</tr>".length()));
+ content = removeFirstTr.toString();
+ } for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {
String arg = scenarioArgument.getKey();
if (getInputs().contains(arg)) {
String argument = scenarioArguments.get(arg);
content = StringUtil.replaceAll(content, "@" + arg, argument);
content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);
} else {
throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
}
}
System.out.println("ScenarioTable.call.substitute >>>>>>>>>> content:" + content);
return content;
}

再次编译,运行Fitneese:

耶,一击中的!

具体的代码在 git.oschina.net

6.尝试用Scenario封装TableTable

因为RestFixture是用TableTable实现的,所以我还想用Scenario封装TableTable,以便在使用RestFixture时,可以模块化组织测试步骤。

首先看一个TableTable例子:

!define TEST_SYSTEM {slim}

!path D:\git\FitnesseKit\RestFixture\target\dependencies\*
!path D:\git\FitnesseKit\RestFixture\target\classes
!path D:\git\FitnesseKit\RestFixture\extra\slf4j-simple-1.6.6.jar | import |
| smartrics.rest.fitnesse.fixture | 获取开始时间
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | begin | js | (new Date()).getTime() | | 调用某个服务,这里用 sleep 5秒 模拟
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | sleepMiliSeconds | js | {{{
var start = (new Date()).getTime();
var now;
do {
now = (new Date()).getTime();
} while(now - start < 5000);
now - start }}} | | 获取结束时间
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | end | js | (new Date()).getTime() | | 打印调用服务所花时间
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | spendSeconds | js | (%end% - %begin%) / 1000 | |

测试结果是这样的:

本测试用例的主要目的是检查调用某个服务所花的时间,本例子是5秒。

接下去我想把上面的获取当前时间,调用服务,计算所花时间都写成Scenario,然后用Script调用Scenario,使测试步骤具有良好的可读性:

!define TEST_SYSTEM {slim}

!path D:\git\FitnesseKit\RestFixture\target\dependencies\*
!path D:\git\FitnesseKit\RestFixture\target\classes
!path D:\git\FitnesseKit\RestFixture\extra\slf4j-simple-1.6.6.jar | import |
| smartrics.rest.fitnesse.fixture | 获取当前时间
| scenario | getTime | _t |
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | @{_t} | js | (new Date()).getTime() | | 计算所花时间
| scenario | spendSeconds | _s || beginTime || endTime |
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | @{_s} | js | (@{endTime} - @{beginTime}) / 1000 | | 调用某个服务,用sleep模拟
| scenario | sleep | s |
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | sleepMiliSeconds | js | {{{
var start = (new Date()).getTime();
do {
var now = (new Date()).getTime();
} while(now - start < @{s} * 1000);
now - start }}} | | 打印调用某个服务所花时间
| script |
| getTime | begin |
| sleep | 5 |
| getTime | end |
| spendSeconds | spend || %begin% || %end% |

测试结果是这样的:

保存内容是The instance scriptTableActor. does not exist,意思为从已定义的script中找不到。

修改ScenarioTable.java后,测试结果:

ScenarioTable.java的主要修改内容:

请到git.oschina.net具体查看。

介绍并扩展Fitnesse的测试模块化机制:ScenarioTable的更多相关文章

  1. 扩展Fitnesse的ScriptTable:支持if-then

    Fitnesse的ScriptTable只能顺序执行所有行,本博文介绍如何让ScriptTable支持if-then,来条件执行一行. 首先普及一下概念,什么是Fitnesse,听一听.NET版Cuc ...

  2. 现代富文本编辑器Quill的模块化机制

    DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师.官方网站:devui.designNg组件库:ng-devui(欢迎S ...

  3. Chrome扩展开发之二——Chrome扩展中脚本的运行机制和通信方式

    目录: 0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述 1.Chrome扩展开发之一——Chrome扩展的文件结构 2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制 ...

  4. Linux模块化机制和module_init

    致谢: 微信公众号:嵌入式企鹅圈 每天都新增爱好者关注,感谢大家的支持和大牛们的建议. 本人将竭力出品很多其它优质的原创文章回馈大家的厚爱. 引子:模块化机制长处 模块化机制(module)是Linu ...

  5. Quill编辑器介绍及扩展

    从这里进入官网. 能找到这个NB的编辑器是因为公司项目需要一个可视化的cms编辑器,类似微信公众号编辑文章.可以插入各种卡片,模块,问题,图片等等.然后插入的内容还需要能删除,拖拽等等.所以采用vue ...

  6. node.js介绍及Win7环境安装测试(转)

    官网描述: Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable ...

  7. windows下php7.1安装redis扩展以及redis测试使用全过程

    最近做项目,需要用到redis相关知识.在Linux下,redis扩展安装起来很容易,但windows下还是会出问题的.因此,特此记下自己实践安装的整个过程,以方便后来人. 一,php中redis扩展 ...

  8. I/O 机制的介绍(Linux 中直接 I/O 机制的介绍)

    IO连接的建立方式 1.缓存IO.流式IO: 2.映射IO.块式IO: 3.直接IO. IO的方式: 同步.异步.定时刷新: MMAP与内核空间 mmap使用共享用户空间与内核空间实现: 直接 I/O ...

  9. windows下php7.1安装redis扩展以及redis测试使用全过程(转)

    最近做项目,需要用到redis相关知识.在Linux下,redis扩展安装起来很容易,但windows下还是会出问题的.因此,特此记下自己实践安装的整个过程,以方便后来人. 一,php中redis扩展 ...

随机推荐

  1. Qt单元测试

    单元测试之作用要完成测试用例,保证设计上的耦合依赖通过测试用例,保证覆盖率,提高程序质量 QTest一些有用的静态函数QTest::qExecQTest::qSleepQTest::qWait   例 ...

  2. poj 2060 Taxi Cab Scheme (最小路径覆盖)

    http://poj.org/problem?id=2060 Taxi Cab Scheme Time Limit: 1000MS   Memory Limit: 30000K Total Submi ...

  3. 1176: [Balkan2007]Mokia - BZOJ

    Description维护一个W*W的矩阵,每次操作可以增加某格子的权值,或询问某子矩阵的总权值. 修改操作数M<=160000,询问数Q<=10000,W<=2000000.Inp ...

  4. arguments.callee查询调用b函数的是哪个函数

    // function functionname(){ // function b(){ // console.log(arguments.callee.caller.name); // } // b ...

  5. PAT-乙级-1012. 数字分类 (20)

    1012. 数字分类 (20) 时间限制 100 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 CHEN, Yue 给定一系列正整数,请按要求对数字进 ...

  6. 下一代Jquery模板-----JsRender

    在ASP.NET MVC利用PagedList分页(二)PagedList+Ajax+JsRender中提到了JsRender.JsRedner和JsViews(JsViews是再JsRender基础 ...

  7. uva 825

    这个......小学生就会的  坑在输入输出了  两个数之间可能不止一个空格....wa了好几遍啊 #include <cstdio> #include <cstring> # ...

  8. 几款开源的图形界面库(GUI Libraries)

    SmartWin++ 遵循BSD许可协议的C++ GUI库,建立在Windows API之上,但仍可以通过使用WineLib在Linux/xNix上使用.也支持Pocket PC和基于Windows ...

  9. POJ 1942 Paths on a Grid(组合数)

    http://poj.org/problem?id=1942 题意 :在一个n*m的矩形上有n*m个网格,从左下角的网格划到右上角的网格,沿着边画,只能向上或向右走,问有多少条不重复的路 . 思路 : ...

  10. http://wenku.baidu.com/link?url=UGoPtZviipHzi5SDIlGx6hPFWAHTPLFXcZ7ieD15JMd81DEHqjehvphVMhqELmOK4qXR74dTT9nW8VBoApBc7Kfb1ZWrNF_i24fY1YRHVki

    http://wenku.baidu.com/link?url=UGoPtZviipHzi5SDIlGx6hPFWAHTPLFXcZ7ieD15JMd81DEHqjehvphVMhqELmOK4qXR ...