介绍并扩展Fitnesse的测试模块化机制:ScenarioTable

摘要:
然而,Scenario仅限于封装脚本测试步骤。必须先创建脚本实例,然后才能调用它们;方案也不能封装表。本文的后半部分展示了如何修改Fitneese代码并扩展Scenario的打包范围。FIT只识别HTML,而Fitnesse通过提供wiki语法简化了编写测试的过程。在FIT/Ffitnesse中,所有测试都以表格的形式呈现。FitNesse比Cucumber的优势是对Wiki的支持。

摘要:在验收测试框架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的测试结果:

DecisionTable调用Scenario

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

ScriptTable调用Scenario

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

3.Scenario的局限

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

创建Script实例

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

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

没有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?

stackoverflow

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

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

Scenario的源代码在目录D:gitFitnesseKitfitnessesrcfitnesse estsystemsslim ables下:

scenario-src

打开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:gitFitnesseKitfitnessesrcfitnesse estsystemsslim ablesSlimTable.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:gitFitnesseKitfitnesse>ant compile
...
D:gitFitnesseKitfitnesse>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:gitFitnesseKitRestFixture	argetdependencies*
!path D:gitFitnesseKitRestFixture	argetclasses
!path D:gitFitnesseKitRestFixtureextraslf4j-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 | |

测试结果是这样的:

RestFixture测试结果

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

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

!define TEST_SYSTEM {slim}

!path D:gitFitnesseKitRestFixture	argetdependencies*
!path D:gitFitnesseKitRestFixture	argetclasses
!path D:gitFitnesseKitRestFixtureextraslf4j-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% |

测试结果是这样的:

scriptTableActor. does not exist

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

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

Scenario封装Table

ScenarioTable.java的主要修改内容:

ScenarioTable.java的主要修改内容

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

免责声明:文章转载自《介绍并扩展Fitnesse的测试模块化机制:ScenarioTable》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇SIGAI深度学习第六集 受限玻尔兹曼机C#操作Xml:XPath语法 在C#中使用XPath示例下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

关于layui中tablle 渲染数据后 sort排序问题

table.render({ id:'test',elem: '#test' //,height: 350 ,url: "/admin/members/list/" , request:{ pageName: 'page' //页码的参数名称,默认:...

pg10、11、12版本特性

2. 分区表的改进 PostgreSQL 10 实现了声明式分区,PostgtreSQL 11完善了功能,PostgreSQL 12提升了性能。我们知道在PostgreSQL 9.X时代需要通过表继承实现分区,这时还需要手工加触发器或规则把新插入的数据重新定向到具体的分区中,从PostgreSQL 10之后不需要这样了,直接用声明式分区就可以了,语法如下:...

jquery.validate.js的Validate表单验证

原文连接 https://blog.csdn.net/weixin_42765270/article/details/84591593 validate 一、 validate的使用步骤 引入jquery.min.js 引入 jquery.validate.js 页面加载后对表单进行验证 $("#表单id名").validate({}) 在valid...

table 隐藏某一个td时,显示错乱问题

1、当表格为多列的情况下,属性为”display:block”行的内容宽度仅与第一列宽度相同,也就是说无论你使colspan的属性值为多少,剩余列的空间都不进行解析。2、同一行反复的在”display:none;”与”display:block;”两个状态间切换时,表格的底部会持续的产生多余的空白空间以至于造成页面布局的扭曲。 解决方法:1、用displa...

HashMap在并发下可能出现的问题分析

我们都知道,HashMap在并发环境下使用可能出现问题,但是具体表现,以及为什么出现并发问题,可能并不是所有人都了解,这篇文章记录一下HashMap在多线程环境下可能出现的问题以及如何避免。 在分析HashMap的并发问题前,先简单了解HashMap的put和get基本操作是如何实现的。 1.HashMap的put和get操作 大家知道HashMap内部实...

CetOS 服务不支持 chkconfig 的解决方法

  今天在添加Elasticsearch系统自动启动服务的时候,提示 “服务 elasticsearch 不支持 chkconfig ”,如下图:           后来查找了下原因,是脚本编写的不符合规范,缺少关键的前三两行;   那么前三行是什么作用呢?   示例,前三行如下: #!/bin/bash #chkconfig: 2345 80 90...