当前位置:   article > 正文

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

fitnesse scenario table 示例代码

摘要:在验收测试框架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文本:

  1. !define TEST_SYSTEM {slim}
  2. !path classes
  3. |import|
  4. |fitnesse.slim.test|
  5. !4 定义scenario checkLogin: 登录并检查结果
  6. | scenario | checkLogin | u || p || ensure || logged |
  7. | @{ensure} | login with username | @{u} | and password | @{p} |
  8. | check @{logged} | login message | @{u} logged in. |
  9. | show | number of login attempts |
  10. !4 创建script实例,后面调用scenario都是针对这个实例
  11. | script | login dialog driver | Bob | xyzzy |
  12. !4 Invoking a scenario from a !-DecisionTable-!
  13. | checkLogin |
  14. | u | p | ensure | logged |
  15. | Bob | xyzzy | ensure | |
  16. | Bob | zzyxx | reject | not |
  17. | Cat | xyzzy | reject | not |
  18. !4 Invoking a scenario from a !-ScriptTable-!
  19. | script |
  20. | checkLogin | Bob || zzyxx || reject || not |
  21. | checkLogin | Bob || xyzzy || ensure || |
  22. !4 script原示例
  23. | script | login dialog driver | Bob | xyzzy |
  24. | login with username | Bob | and password | xyzzy |
  25. | check | login message | Bob logged in. |
  26. | reject | login with username | Bob | and password | bad password |
  27. | check | login message | Bob not logged in. |
  28. | check not | login message | Bob logged in. |
  29. | ensure | login with username | Bob | and password | xyzzy |
  30. | note | this is a comment |
  31. | show | number of login attempts |
  32. | $symbol= | login message |
  33. The fixture for this table is:{{{public class LoginDialogDriver {
  34. private String userName;
  35. private String password;
  36. private String message;
  37. private int loginAttempts;
  38. public LoginDialogDriver(String userName, String password) {
  39. this.userName = userName;
  40. this.password = password;
  41. }
  42. public boolean loginWithUsernameAndPassword(String userName, String password) {
  43. loginAttempts++;
  44. boolean result = this.userName.equals(userName) && this.password.equals(password);
  45. if (result)
  46. message = String.format("%s logged in.", this.userName);
  47. else
  48. message = String.format("%s not logged in.", this.userName);
  49. return result;
  50. }
  51. public String loginMessage() {
  52. return message;
  53. }
  54. public int numberOfLoginAttempts() {
  55. return loginAttempts;
  56. }
  57. } }}}

测试用例页面:

测试用例页面

点击Test执行后:

执行结果

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

DecisionTable调用Scenario

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

ScriptTable调用Scenario

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

3.Scenario的局限

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

创建Script实例

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

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

没有Script实例测试结果

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

  1. !4 定义scenario checkLogin: 登录并检查结果
  2. | scenario | checkLogin | u || p || ensure || logged |
  3. | script | login dialog driver | Bob | xyzzy | <--这是新加的创建Script实例的语句
  4. | @{ensure} | login with username | @{u} | and password | @{p} |
  5. | check @{logged} | login message | @{u} logged in. |
  6. | 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:\git\FitnesseKit\fitnesse\src\fitnesse\testsystems\slim\tables下:

scenario-src

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

  1. @Override
  2. public String substitute(String content) throws SyntaxError {
  3. for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {
  4. String arg = scenarioArgument.getKey();
  5. if (getInputs().contains(arg)) {
  6. String argument = scenarioArguments.get(arg);
  7. content = StringUtil.replaceAll(content, "@" + arg, argument);
  8. content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);
  9. } else {
  10. throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
  11. }
  12. }
  13. return content;
  14. }
  15. });

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

  1. @Override
  2. public String substitute(String content) throws SyntaxError {
  3. + System.out.println("ScenarioTable.call.substitute <<<<<<<<<< content:" + content);
  4. for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {
  5. String arg = scenarioArgument.getKey();
  6. if (getInputs().contains(arg)) {
  7. String argument = scenarioArguments.get(arg);
  8. content = StringUtil.replaceAll(content, "@" + arg, argument);
  9. content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);
  10. } else {
  11. throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
  12. }
  13. }
  14. + System.out.println("ScenarioTable.call.substitute >>>>>>>>>> content:" + content);
  15. return content;
  16. }

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

  1. public SlimTable(Table table, String id, SlimTestContext testContext) {
  2. + System.out.println("SlimTable.SlimTable table:"+table);
  3. this.id = id;
  4. this.table = table;
  5. this.testContext = testContext;
  6. tableName = getTableType() + "_" + id;
  7. }

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

使用命令ant compile重新编译Fitnesse,并输入ant run重新启动Fitneese:
D:\git\FitnesseKit\fitnesse>ant compile
...
D:\git\FitnesseKit\fitnesse>ant run

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

  1. [java] ScenarioTable.call.substitute <<<<<<<<<< content:<table>
  2. [java] <tr>
  3. [java] <td>scenario</td>
  4. [java] <td>checkLogin</td>
  5. [java] <td>u</td>
  6. [java] <td></td>
  7. [java] <td>p</td>
  8. [java] <td></td>
  9. [java] <td>ensure</td>
  10. [java] <td></td>
  11. [java] <td>logged</td>
  12. [java] </tr>
  13. [java] <tr>
  14. [java] <td>Script</td>
  15. [java] <td>login dialog driver</td>
  16. [java] <td>Bob</td>
  17. [java] <td colspan="6">xyzzy</td>
  18. [java] </tr>
  19. [java] <tr>
  20. [java] <td>@{ensure}</td>
  21. [java] <td>login with username</td>
  22. [java] <td>@{u}</td>
  23. [java] <td>and password</td>
  24. [java] <td colspan="5">@{p}</td>
  25. [java] </tr>
  26. [java] <tr>
  27. [java] <td>check @{logged}</td>
  28. [java] <td>login message</td>
  29. [java] <td colspan="7">@{u} logged in.</td>
  30. [java] </tr>
  31. [java] <tr>
  32. [java] <td>show</td>
  33. [java] <td colspan="8">number of login attempts</td>
  34. [java] </tr>
  35. [java] </table>
  36. [java] ScenarioTable.call.substitute >>>>>>>>>> content:<table>
  37. [java] <tr>
  38. [java] <td>scenario</td>
  39. [java] <td>checkLogin</td>
  40. [java] <td>u</td>
  41. [java] <td></td>
  42. [java] <td>p</td>
  43. [java] <td></td>
  44. [java] <td>ensure</td>
  45. [java] <td></td>
  46. [java] <td>logged</td>
  47. [java] </tr>
  48. [java] <tr>
  49. [java] <td>Script</td>
  50. [java] <td>login dialog driver</td>
  51. [java] <td>Bob</td>
  52. [java] <td colspan="6">xyzzy</td>
  53. [java] </tr>
  54. [java] <tr>
  55. [java] <td>ensure</td>
  56. [java] <td>login with username</td>
  57. [java] <td>Bob</td>
  58. [java] <td>and password</td>
  59. [java] <td colspan="5">xyzzy</td>
  60. [java] </tr>
  61. [java] <tr>
  62. [java] <td>check </td>
  63. [java] <td>login message</td>
  64. [java] <td colspan="7">Bob logged in.</td>
  65. [java] </tr>
  66. [java] <tr>
  67. [java] <td>show</td>
  68. [java] <td colspan="8">number of login attempts</td>
  69. [java] </tr>
  70. [java] </table>
  71. [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:

  1. | Script | login dialog driver | Bob | xyzzy |
  2. | ensure | login with username | Bob | and password | xyzzy |
  3. | check | login message | Bob logged in. |
  4. | 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函数:

  1. public String substitute(String content) throws SyntaxError {
  2. System.out.println("ScenarioTable.call.substitute <<<<<<<<<< content:" + content);
  3. + int trLeftFirstIndex = content.indexOf("<tr>");
  4. + int trRightFirstIndex = content.indexOf("</tr>");
  5. + int trLeftSecondIndex = content.indexOf("<tr>", trLeftFirstIndex + 1);
  6. + int trRightSecondIndex = content.indexOf("</tr>", trRightFirstIndex + 1);
  7. + int scriptIndex = content.toLowerCase().indexOf("<td>script</td>");
  8. + if(scriptIndex > trLeftSecondIndex && scriptIndex < trRightSecondIndex) {
  9. + StringBuffer removeFirstTr = new StringBuffer();
  10. + removeFirstTr.append(content.substring(0, trLeftFirstIndex));
  11. + removeFirstTr.append(content.substring(trRightFirstIndex + "</tr>".length()));
  12. + content = removeFirstTr.toString();
  13. + }
  14. for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {
  15. String arg = scenarioArgument.getKey();
  16. if (getInputs().contains(arg)) {
  17. String argument = scenarioArguments.get(arg);
  18. content = StringUtil.replaceAll(content, "@" + arg, argument);
  19. content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);
  20. } else {
  21. throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));
  22. }
  23. }
  24. System.out.println("ScenarioTable.call.substitute >>>>>>>>>> content:" + content);
  25. return content;
  26. }

再次编译,运行Fitneese:

运行成功

耶,一击中的!

具体的代码在 git.oschina.net

6.尝试用Scenario封装TableTable

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

首先看一个TableTable例子:

  1. !define TEST_SYSTEM {slim}
  2. !path D:\git\FitnesseKit\RestFixture\target\dependencies\*
  3. !path D:\git\FitnesseKit\RestFixture\target\classes
  4. !path D:\git\FitnesseKit\RestFixture\extra\slf4j-simple-1.6.6.jar
  5. | import |
  6. | smartrics.rest.fitnesse.fixture |
  7. 获取开始时间
  8. | Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
  9. | let | begin | js | (new Date()).getTime() | |
  10. 调用某个服务,这里用 sleep 5秒 模拟
  11. | Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
  12. | let | sleepMiliSeconds | js | {{{
  13. var start = (new Date()).getTime();
  14. var now;
  15. do {
  16. now = (new Date()).getTime();
  17. } while(now - start < 5000);
  18. now - start }}} | |
  19. 获取结束时间
  20. | Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
  21. | let | end | js | (new Date()).getTime() | |
  22. 打印调用服务所花时间
  23. | Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
  24. | let | spendSeconds | js | (%end% - %begin%) / 1000 | |

测试结果是这样的:

RestFixture测试结果

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

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

  1. !define TEST_SYSTEM {slim}
  2. !path D:\git\FitnesseKit\RestFixture\target\dependencies\*
  3. !path D:\git\FitnesseKit\RestFixture\target\classes
  4. !path D:\git\FitnesseKit\RestFixture\extra\slf4j-simple-1.6.6.jar
  5. | import |
  6. | smartrics.rest.fitnesse.fixture |
  7. 获取当前时间
  8. | scenario | getTime | _t |
  9. | Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
  10. | let | @{_t} | js | (new Date()).getTime() | |
  11. 计算所花时间
  12. | scenario | spendSeconds | _s || beginTime || endTime |
  13. | Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
  14. | let | @{_s} | js | (@{endTime} - @{beginTime}) / 1000 | |
  15. 调用某个服务,用sleep模拟
  16. | scenario | sleep | s |
  17. | Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
  18. | let | sleepMiliSeconds | js | {{{
  19. var start = (new Date()).getTime();
  20. do {
  21. var now = (new Date()).getTime();
  22. } while(now - start < @{s} * 1000);
  23. now - start }}} | |
  24. 打印调用某个服务所花时间
  25. | script |
  26. | getTime | begin |
  27. | sleep | 5 |
  28. | getTime | end |
  29. | 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具体查看。

转载于:https://www.cnblogs.com/fitnessefan/p/3913478.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/706840
推荐阅读
相关标签
  

闽ICP备14008679号