zoukankan      html  css  js  c++  java
  • 介绍并扩展Fitnesse的测试模块化机制:ScenarioTable

    摘要:在验收测试框架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具体查看。

  • 相关阅读:
    平衡二叉树
    2020年度总结
    go中string是如何实现的呢
    escape的编码解码
    小程序实现下载图片到手机及文字到手机粘贴板
    小程序分享(单页面,朋友圈)
    sql server单行拆分成多行
    sql server 查询分组后用逗号拼接字符串和拆分
    安装虚拟机
    Lombok插件安装
  • 原文地址:https://www.cnblogs.com/fitnessefan/p/3913478.html
Copyright © 2011-2022 走看看