在验收测试框架Fitneese中,使用Scenario可以把最常用的测试步骤封装起来,从而达到模块化定义Fitnesse测试用例的能力。但Scenario仅限于封装Script测试步骤,Script实例要先创建,然后才能调用;Scenario也不能封装Table。 |
首先普及一下概念,什么是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的:
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是怎么替换的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Override
publicString substitute(String content)throwsSyntaxError {
for(Map.Entry 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{
thrownewSyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
}
}
returncontent;
}
});
|
增加两行打印System.out.println:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Override
publicString substitute(String content)throwsSyntaxError {
+ System.out.println("ScenarioTable.call.substitute <<<<<<<<<< content:"+ content);
for(Map.Entry 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{
thrownewSyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
}
}
+ System.out.println("ScenarioTable.call.substitute >>>>>>>>>> content:"+ content);
returncontent;
}
|
在D:\git\FitnesseKit\fitnesse\src\fitnesse\testsystems\slim\tables\SlimTable.java的构造函数SlimTable中增加一行打印:
1
2
3
4
5
6
7
|
publicSlimTable(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:
1
2
3
|
D:\git\FitnesseKit\fitnesse>ant compile
...
D:\git\FitnesseKit\fitnesse>ant run
|
再次运行刚刚失败的测试,现在看命令行打印:
<pre> [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]]
</pre>
再去运行一个没有被Scenario的封装的Script:
1
2
3
4
|
| 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 |
|
命令行打印如下内容:
1
|
[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(""); + int trRightFirstIndex = content.indexOf(""); + int trLeftSecondIndex = content.indexOf("", trLeftFirstIndex + 1); + int trRightSecondIndex = content.indexOf("", trRightFirstIndex + 1); + int scriptIndex = content.toLowerCase().indexOf("script"); + if(scriptIndex > trLeftSecondIndex && scriptIndex < trRightSecondIndex) { + StringBuffer removeFirstTr = new StringBuffer(); + removeFirstTr.append(content.substring(0, trLeftFirstIndex)); + removeFirstTr.append(content.substring(trRightFirstIndex + "".length())); + content = removeFirstTr.toString(); + } for (Map.Entry 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:
耶,一击中的!