cucumberJS 行为驱动开发BDD

JavaScript 中的 BDD : Cucumber 和 Gherkin 入门

每个人都听说过测试驱动开发(TDD),以及它将对整个产品和开发周期带来的好处。这些都是显而易见的。每次你为一段代码编写测试,你将知道代码是否正常运行。而且,更重要的是,以后你将第一时间知道代码是否发生中断。

行为驱动开发(BDD)是对此理念的扩展,但不同的是它并不是测试代码,而是测试产品,特别是产品是否按照你的期望行为去运行。(场景实例化)

在本文中,我将向你介绍如何搭建并运行Cucumber,该框架被用来运行以BDD风格编写的自动化验收测试。这种测试的优势在于,它们可以用简单的英文/中文(支持多国语言)来编写,因此参与项目中的非技术人员也可以理解它。在阅读本文完毕后,你可以决定Cucumber是否适用与你以及你的团队,并开始编写你自己的验收测试。

BDD vs TDD — 有什么不同?

【区别主要是在测试的结构和写法上】

在TDD的设定里,测试由编写被测代码的开发人员编写、维护和理解。一般来说不需要其他人去理解测试,这很棒。

在BDD的设定里,测试需要被除了编写功能的开发人员之外的更多人理解。 有更多的项目参与者对产品的功能感兴趣。

这些人员可能包括质量保证人员、产品分析师、销售人员,甚至是高级管理人员。

这是两者之间的区别:

TDD

const assert = require('assert');
const webdriver = require('selenium-webdriver');
const browser = new webdriver.Builder()
  .usingServer()
  .withCapabilities({'browserName': 'chrome' })
  .build();

browser.get('http://en.wikipedia.org/wiki/Wiki');
browser.findElements(webdriver.By.css('[href^="/wiki/"]'))
.then(function(links){
  assert.equal(19, links.length); // Made up number
  browser.quit();

});

BDD

Given I have opened a Web Browser
When I load the Wikipedia article on "Wiki"
Then I have "19" Wiki Links

这两种测试做了同样的事,但是一种是通俗易懂的,另一种只有懂得JavaScript和Selenium的人才能阅读。

这篇文章将向你展现如何使用Cucumber.js框架,在你的JavaScript项目中实现BDD测试,从而使你的产品从这种级别的测试中受益。

什么是Cucumber和Gherkin?

Cucumber是一个为行为驱动开发设计的测试框架。它允许你以Gherkin的形式定义你的测试,然后将这些可执行的Gherkin脚本与代码绑定。

Gherkin是一种用来编写Cucumber测试的特定领域语言。它允许测试脚本以人类可读的格式编写,并在参与产品开发的所有人员之间共享。

Gherkin文件包含用Gherkin语言编写的测试。这些文件通常具有.feature的文件扩展名。Gherkin文件的内容通常被简称为“gherkins”。

Gherkins

在一个Gherkin定义的测试中,存在特征和场景的概念。这类似于其他测试框架中的测试套件和测试用例,被用来以一种简洁的方式去构建测试。

一个场景实际上只是一个单一的测试。它应该在你的应用程序中只测试一件事。

一个功能是一组相关的场景。因此,它会在你的应用程序中测试许多相关的东西。理想情况下,Gherkin文件中的功能将紧密映射到应用程序中的功能 — Gherkin由此得名。

每个Gherkin文件只包含一个功能,每个功能都包含一个或多个场景。

然后场景由步骤组成,这些步骤以特定的方式排序:

Given – 这些步骤用于在测试之前设置初始状态

When – 这些步骤是要实际执行的测试

Then –  这些步骤被用来断言测试的结果

上面其实就叫Feature(剧本)文件,其遵循的Gherkin(剧本语法)标准。

这种剧本文件,客户、项目经理、业务分析师,QA都能看懂,因为其就是一个故事点或者需求点。而且通过特定的工具,比如Cucumber,
或CukeTest等工具,能够把其自动转换成为代码。开发人员根据自动生成的代码,断言一些预期的行为,并根据这些行为,完成相应的代码实现。
这样的自动化脚本,为一个项目中的各个人员了解项目的需求,实现提供了一个很好的交互桥梁。

下面是其一个交互的过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ouYRgxRn-1618134780207)(http://thyrsi.com/t6/645/1546154825x2728278644.jpg)]

如果是传统的方式,其交互方式,应该是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YImO3k1M-1618134780210)(http://thyrsi.com/t6/645/1546154898x2728278644.jpg)]

为什么要行为驱动开发(BDD)

理想情况下,每个场景应该是单个测试用例,因此When步骤的数量应该保持很少。

步骤都是可选的。例如,如果你完全不需要在测试开始前设置任何内容,你可以不设置Given步骤。

Gherkin文件是为人类可读设计的,并为参与产品开发的任何人带来益处。由于参与人员包括非技术人员,因此Gherkin文件应始终使用商业语言而不是技术语言编写。这意味着,例如,编写测试时,你不需要去引用某个UI组件,而是要去描述你想要测试的产品的功能。

行为驱动开发是一个软件工程的系列实践,能够帮助团队快速构建和交付更多价值和质量的软件产品。其和敏捷已经精益的开发实践,特别是测试驱动开发(TDD)和领域驱动开发(DDD),是一脉相承的。但是最重要的是BDD提供了一种通用的,简单的,结构化的描述语言,这种语言既可以是英语也可以是其他本地的语言,通过他能够很方便让项目成员和业务干系人非常顺畅的沟通需求,即使这些成员不懂的任何编程语言。

通过对比,可以发现BDD的这种方式,把用户或者客户真正的通过Feature文件联系在一起了,其沟通是顺畅的,各个角色,包括QA,BA,开发,测试,客户,用户可以通过这一媒介,进行高效无障碍的沟通,而不是像传统的方式,通过BA进行二次转达,从而丢失了很多重要的需求。

由此可见,其BDD的好处如下:

减少浪费!

节省成本!

容易并且安全的适应变化!

因为少了中间的转达环节,从而能够快速交付产品!

我们来看下面这个流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1aXYMafK-1618134780212)(http://thyrsi.com/t6/645/1546155203x2728278644.jpg)]
从上图可以看出,当一个需求过来的时候,先通过项目干系人都能理解的Feature文件,描述项目的User Story,有的里面还有详细生动的数据示例(example),从而能够让所有的人更加容易理解其需求,比如:

Scenario Outline: Earning interest
  Given I have an account of type  with a balance of 
  When the monthly interest is calculated
  Then I should have earned at an annual interest rate of 
  And I should have a new balance of 
  Example:
  | initial-balance | account-type | interest-rate | new-balance |
  | 10000           | current      | 1             | 10008.33    |
  | 10000           | savings      | 3             | 10025       |
  | 10000           | supersaver   | 5             | 10041.67    |

通过上面的示例表(example)的表格可以更加容易的理解当前用例的意图了。
当Feature和Example文件都完成后,借助于第三方的开源框架实现,比如Cucumber,CukeTest、jBehave,SpecFlow等把Feature和Example转换成代码,
然后通过底层次的单元测试框架,比如JUnit,mocha,NUnit,Spock,RSpec,结合测试驱动开发,从而把业务代码的逻辑实现一举多得。

一个简单的Gherkin测试

以下是一个使用Google搜索Cucumber.js的简单的Gherkin脚本的示例:

Given I have loaded Google
When I search for "cucumber.js"
Then the first result is "GitHub - cucumber/cucumber-js: Cucumber for JavaScript"

我们可以直截了当的看到,这个测试告诉我们该做什么,而不是如何去做。它是用面对所有读者的语言编写的,而且 — 重要的是 — 无论产品最终 如何调整,这些测试很可能依然是正确的。谷歌可能会彻底改变他们的页面UI,但只要功能不变,那么Gherkin脚本依然是正确的。

你可以在Cucumber wiki上阅读更多关于Given When Then的内容。

在上面的示例中,对应将执行js文件中以下步骤:

Given('I have loaded Google', function() {});
When('I search for {stringInDoubleQuotes}', function() {});
Then('the first result is {stringInDoubleQuotes}', function() {});

不用担心这些代码的含义 – 稍后会详细解释。

本质上,它定义了一些Cucumber.js将代码绑定到Gherkin文件中的步骤的方法。

Cucumber.js

当你使用Gherkin来编写测试用例后,你需要一些方法来执行它们。在JavaScript世界中,有一个叫做Cucumber.js的模块,可以让你做到这一点。它的工作原理是允许你定义可关联到你的Gherkin文件中定义的各个步骤的JavaScript代码。然后通过加载Gherkin文件,以正确的顺序执行与每个步骤相关的JavaScript代码来运行测试。

环境要求

首先,你想要使用cucumber.js模块,就需要安装nodejs并配置相关环境变量(因为这些模块都是基于nodejs
进行封装打包的模块。)

nodejs下载

nodejs环境变量配置

其次,将Cucumber.js添加到你的版本中,在版本中加入Cucumber.js,只需要添加cucumber模块,然后配置并运行。

npm install cucumber //用于项目中的本地安装。

npm install -g cucumber //用于全局安装(服务器部署强烈建议使用全局安装cucumber模块)。

npm install --save-dev cucumber 用于//安装Cucumber.js作为开发依赖项

手动执行

Cucumber.js包含一个可执行文件来运行这些功能。在项目中安装Cucumber之后,您可以使用以下命令运行它:

$ ./node_modules/.bin/cucumber-js
关于全局安装的注意事项:全局安装时 Cucumber不起作用,因为支持文件中需要黄瓜,并且不需要全局安装的模块。

手动执行Cucumber是相对容易的,最好是先确保你能做到这一点,因为以下解决方案都是自动执行相同的方法。

安装后,可执行文件将为./node_modules/.bin/cucumber-js。当你运行它时,它需要知道文件系统上的哪个地方可以找到它所需的所有文件。这些文件都是Gherkin文件和要执行的JavaScript代码。

按照惯例,所有的Gherkin文件都将保存在features目录中,如果不指定,那么Cucumber将在同一个目录中查找要执行的JavaScript代码。指定Cucumber在哪里查找这些文件是一个明智的做法,因为这样可以更好地控制构建过程,比如你指向运行某个文件夹下的测试用例。

例如,如果将所有的Gherkin文件保存在myFeatures目录中,并将所有JavaScript代码保存在mySteps中,则可以执行以下操作:

$ ./node_modules/.bin/cucumber-js ./myFeatures -r ./mySteps

-r标志是一个包含JavaScript文件的目录,以便为自动测试所需。还有其他一些标志可能会引起关注 — 请阅读帮助文本以了解它们的工作原理:

或者你也可以进入项目中,直接执行

 $ ./node_modules/.bin/cucumber-js

当然,不指定目录路径,会在当前项目中全局扫描所有feature文件并进行执行。(这些目录以递归方式进行扫描,因此你可以根据特定情况对文件进行浅层或深层嵌套)

运行时,其他可带入参数

1、需要支持文件

--require 在执行功能之前需要支持文件。

使用glob模式。如果未使用,则需要以下文件:
如果功能存在于features目录中(在任何级别)
    features/**/*.js
除此以外
    /**/*.js 对于包含所选功能的每个目录
指定此选项时将禁用自动加载,并且所有加载都将变为显式。

2、格式
...

其他参数,请参考这里

npm 脚本

一旦手动运行Cucumber,你也可以将它作为npm脚本添加到构建中,你只需要添加下面的命令 — 路径并不是限定的,因为npm会为你处理 — 在你的package.json中,如下所示:

【执行所有feature】

"scripts": {
  "start": "./node_modules/.bin/cucumber-js"
}

这些做完了,你就可以执行:

$ npm run start

$ npm start

它就会像之前一样执行你的Cucumber测试。

【执行指定文件夹feature】

$ cucumber-js features/**/*.feature     //指定glob模式

$ cucumber-js features/dir              //指定功能目录

$ cucumber-js features/my_feature.feature   //指定要素文件

$ cucumber-js features/my_feature.feature:3 //按行号指定方案

$ cucumber-js --name "topic 1"              //通过名称与正则表达式匹配来指定方案,如果多次使用,则方案名称只需匹配提供的名称之一

同样可以放到package.json中

"scripts": {
  "cucumber": "./node_modules/.bin/cucumber-js ./myFeatures -r ./mySteps"
}

这些做完了,你就可以执行:

$ npm run cucumber

$ npm start

它就会像之前一样执行你的Cucumber测试。

特别说明:cucumber 5.0.3之后运行方式cucumber.js变成了cucumber-js!
如果你使用的版本比较低,那么你的执行命令应该是"./node_modules/.bin/cucumber.js"

IDEA插件运行

在IDEA编辑器中搜索 cucumber.js插件并安装,你也可以从这边下载cucumber.js插件

添加一个cucumber.js运行配置,并设置相关参数,IDEA官方参数配置介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R6QYSLEa-1618134780214)(http://thyrsi.com/t6/647/1546353493x2890211744.jpg)]

你可能会遇到一个版本问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nPFThpNh-1618134780215)(http://thyrsi.com/t6/647/1546353701x2890211744.jpg)]

解决办法:IDEA使用最新版、cucumber.js插件使用最新版
Grunt(不推荐)

的确存在用于执行Cucumber.js测试的Grunt插件。然而不幸的是,【它已经过时了】,并且不适用于最新版本的Cucumber.js,这意味着如果你使用它,你将错过许多新的改进。

相反,我的首选方法是使用grunt-shell插件以与上述完全相同的方式执行命令。

安装完成后,配置,这只是将以下插件配置添加到您的Gruntfile.js中的一种情况:

shell: {
  cucumber: {
    command: 'cucumber-js ./myFeatures -r ./mySteps'
  }
}

现在,和之前一样,你可以通过运行grunt shell:cucumber来执行你的测试。

Gulp(不推荐)

Gulp与Grunt的情况完全相同,现有的插件已经过时并且会使用旧版本的Cucumber工具。同样的,在Gulp中你可以使用gulp-shell模块来执行Cucumber.js命令,就像其他场景一样。

设置一下:

gulp.task(‘cucumber’, shell.task([
‘cucumber-js ./myFeatures -r ./mySteps’
]));
现在,和之前一样,你可以通过运行gulp cucumber来执行你的测试。

第一个Cucumber测试

现在我们知道如何执行Cucumber了,那就让我们编写一个测试。在这个例子中,我们将使用一些精心设计的例子向你展现系统是如何运作的。上,你可以做到更多,比如直接调用你正在测试的代码,对正在运行的服务进行HTTP API调用,或者使用Selenium驱动浏览器来测试你的应用程序。

我们将用一个简单的例子将证明算法有效。一共有两个功能 — 加法和乘法。

假设你已安装配置好nodejs.

首先,先配置好。

$ npm init
$ npm install --save-dev cucumber
$ mkdir features steps

如何执行测试完全取决于你自己。在这个例子中,我只是为了简单起见而手动完成它。在实际的项目中,你可以使用上述选项之一将其集成到版本中。

$ ./node_modules/.bin/cucumber-js features/ -r steps/
0 scenarios
0 steps
0m00.000s
$

现在,让我们来编写我们的第一个feature。首先进入features / addition.feature:

Feature: Addition
  Scenario: 1 + 0
    Given I start with 1
    When I add 0
    Then I end up with 1

  Scenario: 1 + 1
    Given I start with 1
    When I add 1
    Then I end up with 2

很简单,也很容易阅读。 告诉我们我们正在做什么,而不是如何做。 我们来试试吧:

$ ./node_modules/.bin/cucumber-js features/ -r steps/
Feature: Addition

  Scenario: 1 + 0
  ? Given I start with 1
  ? When I add 0
  ? Then I end up with 1

  Scenario: 1 + 1
  ? Given I start with 1
  ? When I add 1
  ? Then I end up with 2

Warnings:

1) Scenario: 1 + 0 - features/addition.feature:3
   Step: Given I start with 1 - features/addition.feature:4
   Message:
     Undefined. Implement with the following snippet:

       Given('I start with {int}', function (int, callback) {
         // Write code here that turns the phrase above into concrete actions
         callback(null, 'pending');
       });

2) Scenario: 1 + 0 - features/addition.feature:3
   Step: When I add 0 - features/addition.feature:5
   Message:
     Undefined. Implement with the following snippet:

       When('I add {int}', function (int, callback) {
         // Write code here that turns the phrase above into concrete actions
         callback(null, 'pending');
       });

3) Scenario: 1 + 0 - features/addition.feature:3
   Step: Then I end up with 1 - features/addition.feature:6
   Message:
     Undefined. Implement with the following snippet:

       Then('I end up with {int}', function (int, callback) {
         // Write code here that turns the phrase above into concrete actions
         callback(null, 'pending');
       });

4) Scenario: 1 + 1 - features/addition.feature:8
   Step: Given I start with 1 - features/addition.feature:9
   Message:
     Undefined. Implement with the following snippet:

       Given('I start with {int}', function (int, callback) {
         // Write code here that turns the phrase above into concrete actions
         callback(null, 'pending');
       });

5) Scenario: 1 + 1 - features/addition.feature:8
   Step: When I add 1 - features/addition.feature:10
   Message:
     Undefined. Implement with the following snippet:

       When('I add {int}', function (int, callback) {
         // Write code here that turns the phrase above into concrete actions
         callback(null, 'pending');
       });

6) Scenario: 1 + 1 - features/addition.feature:8
   Step: Then I end up with 2 - features/addition.feature:11
   Message:
     Undefined. Implement with the following snippet:

       Then('I end up with {int}', function (int, callback) {
         // Write code here that turns the phrase above into concrete actions
         callback(null, 'pending');
       });

2 scenarios (2 undefined)
6 steps (6 undefined)
0m00.000s
$

厉害了!我们刚刚写下了我们的Gherkin,并且一切都在执行。不过它现在还不能起作用,因为我们不知道如何处理这些步骤,但Cucumber非常清楚地告诉了我们。

现在让我们写第一步的文件吧。这将简单地按照Cucumber输出告诉我们的方式来执行这些步骤,这些步骤没有起到实际的作用,但是整理了输出。

此处说明:cucumber.js版本5.0.3 官方不再推荐像下面这么定义Given、When、Then、And在defineSupportCode回调函数中中,控制台会提示"使用defineSupportCode是不赞成,请要求/导入单独的方法"。

liyinchi$ ./node_modules/.bin/cucumber-js
(node:66659) DeprecationWarning: cucumber: defineSupportCode is deprecated. Please require/import the individual methods instead.
..................................

8 scenarios (8 passed)
18 steps (18 passed)
0m00.016s

文件为 steps/maths.js:

const defineSupportCode = require(‘cucumber’).defineSupportCode;

defineSupportCode(function({ Given, Then, When }) {
  Given('I start with {int}', function (int, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
  });
  When('I add {int}', function (int, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
  });
  Then('I end up with {int}', function (int, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
  });
});

defineSupportCode hook是Cucumber.js的一种允许你提供用于各种不同场景的代码的方法。这些步骤都会被覆盖到,每次你想编写Cucumber可以直接调用的代码时,代码必须在某一步骤的代码块中。

注意,示例代码定义了三个不同的步骤 - 分别为Given,When和Then。每一个步骤的代码块都会被赋予一个字符串 - 或者是正则表达式 - 与feature文件中的某个步骤相匹配,并且与该步骤匹配时会执行该函数。占位符可以放入步骤字符串中 - 或者,如果使用正则表达式,则可以使用捕获表达式 - 而这些占位符将被提取出来并作为参数提供给您的函数。

执行此操作提供了更简洁的输出,而实际上并没有做任何事情:

$ ./node_modules/.bin/cucumber-js features/ -r steps/
Feature: Addition

  Scenario: 1 + 0
  ? Given I start with 1
  - When I add 0
  - Then I end up with 1

  Scenario: 1 + 1
  ? Given I start with 1
  - When I add 1
  - Then I end up with 2

Warnings:

1) Scenario: 1 + 0 - features/addition.feature:3
   Step: Given I start with 1 - features/addition.feature:4
   Step Definition: steps/maths.js:4
   Message:
     Pending

2) Scenario: 1 + 1 - features/addition.feature:8
   Step: Given I start with 1 - features/addition.feature:9
   Step Definition: steps/maths.js:4
   Message:
     Pending

2 scenarios (2 pending)
6 steps (2 pending, 4 skipped)
0m00.002s

现在需要让脚本运行起来。我们所需要做的就是在我们的定义的步骤中实现代码。同时也需要整理一下,以便阅读。这本质上是消除了回调参数,因为我们没有做任何异步的操作。

在以上操作完成之后,“steps/maths.js”将如下所示:

const defineSupportCode = require('cucumber').defineSupportCode;
const assert = require('assert');

defineSupportCode(function({ Given, Then, When }) {
  let answer = 0;

  Given('I start with {int}', function (input) {
    answer = input;
  });
  When('I add {int}', function (input) {
    answer = answer + input;
  });
  Then('I end up with {int}', function (input) {
    assert.equal(answer, input);
  });
});

执行如下:

$ ./node_modules/.bin/cucumber-js features/ -r steps/
Feature: Addition

  Scenario: 1 + 0
  ✔ Given I start with 1
  ✔ When I add 0
  ✔ Then I end up with 1

  Scenario: 1 + 1
  ✔ Given I start with 1
  ✔ When I add 1
  ✔ Then I end up with 2

2 scenarios (2 passed)
6 steps (6 passed)
0m00.001s

全部通过。我们现在知道加法正确运行了。

请注意,我们只需编写一小段代码,而Cucumber将它们粘合在一起。
我们可以通过指定步骤代码如何从Gherkin文件执行来得到自动参数化测试。这意味可以非常简单的添加更多的场景。

接下来,让我们证明乘法正确运作。为此,我们需要在features/multiplication.feature中写下如下的Gherkin:

Feature: Multiplication

  Scenario: 1 * 0
    Given I start with 1
    When I multiply by 0
    Then I end up with 0

  Scenario: 1 * 1
    Given I start with 1
    When I multiply by 1
    Then I end up with 1

  Scenario: 2 + 2
    Given I start with 2
    When I multiply by 2
    Then I end up with 4
然后让我们在steps/maths.js中实现新的步骤。为此,我们只需在defineSupportCode方法内添加以下代码块:

When('I multiply by {int}', function (input) {
  answer = answer * input;
});

就是这样。运行此操作将给出以下结果:

$ ./node_modules/.bin/cucumber-js features/ -r steps/
Feature: Addition

  Scenario: 1 + 0
  ✔ Given I start with 1
  ✔ When I add 0
  ✔ Then I end up with 1

  Scenario: 1 + 1
  ✔ Given I start with 1
  ✔ When I add 1
  ✔ Then I end up with 2

Feature: Multiplication

  Scenario: 1 * 0
  ✔ Given I start with 1
  ✔ When I multiply by 0
  ✔ Then I end up with 0

  Scenario: 1 * 1
  ✔ Given I start with 1
  ✔ When I multiply by 1
  ✔ Then I end up with 1

  Scenario: 2 + 2
  ✔ Given I start with 2
  ✔ When I multiply by 2
  ✔ Then I end up with 4

5 scenarios (5 passed)
15 steps (15 passed)
0m00.003s
$

就这么简单,现在我们有了一个用来证明运算的易于扩展的测试套件。作为练习,请尝试扩展它以支持减法。如果遇到困难,你可以在评论中寻求帮助。

官方示例

Cucumber.js的进阶技巧

这些功能都很不错,但是Cucumber的一些进阶功能能让我们的工作更加轻松。

【异步步骤的定义】

到目前为止,我们只写了同步步骤的定义,在JavaScript世界中,这不够!JavaScript中有很多情况下需要是异步的,所以我们需要一些方法来处理它。

值得庆幸的是,Cucumber.js有一些内置的处理方法,具体选择哪一种取决于你的喜好。

上述的方式是JavaScript中处理异步步骤的传统方法,即使用回调函数。如果定义步骤时将回调函数作为其最后一个参数,那么在触发此回调之前该步骤不会被视为完成。在这种情况下,如果回调被任何参数触发,那么这被认为是错误的,并且该步骤将失败。如果它没有任何参数触发,那么该步骤被认为是成功的。但是,如果回调没有被触发,那么框架最终会超时并且失败。本段文字的中心就是,如果你接收了一个回调参数,那么确保你调用它。

例如,使用回调进行HTTP API调用的步骤的定义如下所示。
这是使用Request编写的,因为它使用回调函数来响应。

When('I make an API call using callbacks', function(callbacks) {
  request('http://localhost:3000/api/endpoint', (err, response, body) => {
    if (err) {
      callback(err);
    } else {
      doSomethingWithResponse(body);
      callback();
    }
  });
});

首选方法和替代方法是按返回类型。如果你从你的步骤中返回一个Promise对象,那么只有当Promise状态为settled才认为该步骤已经完成。

如果Promise状态为rejected,那么步骤将失败,如果Promise状态为fulfilled,则步骤将会成功。

或者,如果您返回的不是Promise,那么该步骤将立即被视为已成功。

其中包括返回undefined或null。这意味着您可以在执行该步骤时选择是否需要返回Promise,框架将根据需要进行调整。

例如,使用Promise进行HTTP API调用的步骤定义如下所示。这是使用Fetch API编写的,因为它返回Promise响应。

When('I make an API call using promises', function() {
  return fetch('http://localhost:3000/api/endpoint')
    .then(res => res.json())
    .then(body => doSomethingWithResponse(body));
});

Feature Background

一个Feature Background是一个Gherkin片段,它被预先添加到文件中每个Scenario的开头。这使得我们在每个Scenario之间可以共享常用的步骤,而不需要重复实现它们。

Background是使用Background关键字而不是Scenario关键字编写的。理想情况下,这其中应该只包含Given步骤,因为包含每个测试之间共享的When步骤或Then步骤是没有意义的。但是框架不会在这上面限制你,所以要悉心构造你的测试结构。

使用它,我们可以重新编写我们的添加的feature,如下所示:

Feature: Addition

  Background:
    Given I start with 1

  Scenario: 1 + 0
    When I add 0
    Then I end up with 1

  Scenario: 1 + 1
    When I add 1
    Then I end up with 2

这实际上与之前的脚本完全一样,但是由于我们已经将通用的步骤提取出来了,所以简短一些。

Scenario Outlines

Scenario Outlines是从测试数据表中生成scenario的一种方式。这使得我们能以比以前更高效的方式进行参数化测试,可以使用插入不同值的方式重复同一测试脚本。

Scenario outlines通过使用Scenario Outline关键字而不是Scenario关键字编写,之后提供一个或多个Examples表。然后将来自Examples表格的参数替换到Scenario Outline中以生成运行的scenarios。

使用它,我们可以重构我们的乘法feature,如下所示:

Feature: Multiplication

  Scenario Outline:  * 
    Given I start with 
    When I multiply by 
    Then I end up with 

  Examples:
    | a | b | answer |
    | 1 | 0 | 0      |
    | 1 | 1 | 1      |
    | 2 | 2 | 4      |

再一次,这和以前完全一样,但重复次数要少得多。你会看到你是否运行它,它会在输出中生成与之前完全相同的场景:

Feature: Multiplication

  Scenario: 1 * 0
  ✔ Given I start with 1
  ✔ When I multiply by 0
  ✔ Then I end up with 0

  Scenario: 1 * 1
  ✔ Given I start with 1
  ✔ When I multiply by 1
  ✔ Then I end up with 1

  Scenario: 2 * 2
  ✔ Given I start with 2
  ✔ When I multiply by 2
  ✔ Then I end up with 4

数据表

我们刚刚在scenario outline中看到了一张表格,用于生成我们可以从中生成scenario的数据。
但是,我们也可以在scenario中使用数据表。这些可以用作提供数据表或结构化输入或许多其他事物的方式。

例如,可以将添加方案的方式重写为添加任意自变量的值,如下所示:

Scenario: Add numbers
  Given I start with 0
  When I add the following numbers:
    | 1 |
    | 2 |
    | 3 |
    | 4 |
  Then I end up with 10

针对这个简单的例子,步骤将如下:

When('I add the following numbers:', function (table) {
  answer = table.raw()
    .map(row => row[0])
    .map(v => parseInt(v))
    .reduce((current, next) => current + next, answer);
});

我们提供的table参数是一个DataTable对象,它有一个可以调用的原始方法。此方法返回数据表中所有值的二维数组,因此外部数组中的每个表项都是表中的一行,而内部数组中的每个表项都是该行的单元格 - 一个字符串。

一个更复杂的例子是使用数据表来填充表单。然后使用该表来提供所有的输入,而不是使用难以阅读的步骤定义。这可以读取如下内容:

Scenario: Create a new user
  When I create a new user with details:
    | Username | graham               |
    | Email    | [email protected] |
    | Password | mySecretPassword     |
  Then the user is created successfully

在这种情况下,通过使用rowsHash方法,数据表类可以使我们更容易访问表。

步骤可能为如下所示:

When('I create a new user with details:', function (table) {
  const data = table.rowsHash();
  createUser(data);
});

在这种情况下,data对象将从数据表中解析出来,如下所示:

{
  "Username": "graham",
  "Email": "[email protected]",
  "Password": "mySecretPassword"
}

通过第一列中的键可以轻松访问这些字段。

Hooks 钩子

像大多数测试框架一样,Cucumber.js支持在场景运行之前和之后执行hook。

它们的设置方式与步骤定义相同,只需要简单按名字的调用hook - 在场景运行之前或之后,无论运行成功或失败与否。

一个简单的例子,为了能使我们的feature们更加可靠,编写代码如下:

var { Before,After,Given, When, Then,And} = require('cucumber');

    Before(function() {
        let answer = 0;
    });

    After(function() {
        
    });

如上所述,优化后的数学步骤文件可以确保在每个场景运行之前将answer变量重置为0,这意味着如果我们从0开始则不需要Given步骤。

我们来看下浏览器自动化测试脚本的简单例子:

通过参数化url,

【feature文件】

Feature: Multiplication

  Scenario Outline: open web
    Given 打开浏览器访问 ""
    Examples:
    | url |
    | http://www.baidu.com |
    | http://www.qq.com |
    | http://www.360.com |

【step_definitions文件夹下xxx.js】

var { setDefaultTimeout,Before,After,Given, When, Then,And} = require('cucumber');//引入cucumber
var webdriverio = require('webdriverio'); //引入webdriver库
var expect = require('chai'); //引入断言库
var assert = require('assert'); //引入断言库
var should = require('should');//引入断言库
var client;
var options = {
 desiredCapabilities: {
         //chrome、safari、firefox
        browserName: 'safari' ,
        chromeOptions: {
                    args: [
                        'headless',
                        // Use --disable-gpu to avoid an error from a missing Mesa
                        // library, as per
                        // https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
                        'disable-gpu',
                    ],
                },
    }
};
    setDefaultTimeout(5000);//设置异步步骤的默认超时。默认为5000毫秒。

    Before(function() {
      client=webdriverio
      .remote(options)
      .init();//初始化浏览器对象
    });
    
    
    After(function() {
      client.end();//退出浏览器
    });

    Given('打开浏览器访问 {string}', function(url) {
      client.url(url);//打开网页
      });

  
  
});

运行

$ ./node_modules/.bin/cucumber-js

运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aROCPUaz-1618134780217)(http://thyrsi.com/t6/644/1546050013x2728329023.jpg “运行结果”)]

通过接受回调函数作为第二个参数或返回Promise,你也可以在步骤定义完全相同的方式实现异步钩子。


如果你觉得Before和After简单操作对您来说还不够,那么官方还为我们提供了这些方法:

BeforeFeatures – 在运行任何一个feature文件之前调用一次,并提供了特性列表。
BeforeFeature – 在运行每个feature文件之前调用,提供该feature。
BeforeScenario – 在运行每个场景之前调用,提供了该场景。这大致类似于“之前”钩子。
BeforeStep – 在运行每个步骤之前调用,随步骤一起提供。
StepResult – 在运行每个步骤之后调用,并提供该步骤的结果。
AfterStep – 在运行每个步骤之后调用,该步骤提供。
ScenarioResult – 在每个场景运行之后调用,并提供场景的结果。
AfterScenario – 在每个场景运行后调用,提供了该场景。这大致类似于“后”钩子。
AfterFeature – 在运行每个feature之后调用,并提供该特性。
FeaturesResult – 在运行所有feature内容之后调用一次,并提供运行所有内容的结果。
AfterFeatures – 在运行所有feature内容之后调用一次,并提供了feature列表。

这些hook将与测试框架的整个生命周期进行交互,并将按上面列出的顺序进行调用。

设置超时时间

//全局超时设置
var {setDefaultTimeout} = require('cucumber');
setDefaultTimeout(60 * 1000);

//局部超时设置
var {Before, Given} = require('cucumber');

Before({timeout: 60 * 1000}, function() {
  // Does some slow browser/filesystem/network actions
});

Given(/^a slow step$/, {timeout: 60 * 1000}, function() {
  // Does some slow browser/filesystem/network actions
});

禁用超时时间

除非绝对必要,否则不要使用!

通过将超时设置为-1禁用超时。如果使用这个,则需要实现自己的超时保护。否则,测试套件可能会过早结束或无限期挂起!

var {Before, Given} = require('cucumber');
var Promise = require('bluebird');

Given('the operation completes within {n} minutes', {timeout: -1}, function(minutes) {
  const milliseconds = (minutes + 1) * 60 * 1000
  const message = `operation did not complete within ${minutes} minutes`
  return Promise(this.verifyOperationComplete()).timeout(milliseconds, message);
});

以下是旧文档信息请参考:

CLI

Custom Formatters

Custom snippet syntaxes

NodeJs Example

World

Hook

Timeouts

Data table

Attrachments

Gherkin API参考

FAQ

重要提醒!!defineSupportCode已被弃用详见

使用defineSupportCode方法中的registerHandler方法处理这些hook。代码如下:


defineSupportCode(function({ registerHandler }) {
  registerHandler('BeforeStep', function(step) {
    console.log('About to execute step:' + util.inspect(step));
  });
  registerHandler('ScenarioResult', function(scenario) {
    console.log('Result of Scenario:' + util.inspect(scenario));
  });
});

通过接受回调函数作为第二个参数,或者通过返回一个Promise对象,事件处理程序可以与定义步骤完全相同的方式进行异步处理。

(推荐)直接引入相关Hook,然后函数直接写到js文件中。代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvbHGbMo-1618134780218)(http://thyrsi.com/t6/644/1546066757x2890149637.jpg “”)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JWvY14ZX-1618134780218)(http://thyrsi.com/t6/644/1546049359x2890174076.jpg “正确姿势”)]

World – 共享代码和数据

到目前为止,我们都无法在步骤之间共享代码。我们可以很容易地拥有很多包含步骤定义,hook,event等的JavaScript文件,但它们彼此独立。

碰巧,事实并非如此。Cucumber.js有一个“World”的概念,它是所有运行场景的域。所有的步骤定义、挂钩和事件处理程序都可以通过访问this参数来访问它,无论定义步骤定义的文件如何。这就是为什么所有示例都是使用传统的function关键字编写的,而不是箭头函数。JavaScript中的箭头函数为您重新绑定此变量,这意味着您将无法访问测试中可能需要的World域。

这种方式使得你不通过额外的处理,就可以直接使用它。这也意味着我们可以通过在多个文件之间逻辑分割Cucumber代码并使它们按预期工作,同时仍然可以访问共享域,从而使我们的代码更加整洁。

cucumber识别中文

首先cucumber支持许多国家的语言,查看语言列表

$ ./node_modules/.bin/cucumber-js --i18n-languages

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cYpMyL3q-1618134780219)(http://thyrsi.com/t6/644/1546068162x2890174094.jpg “cucumber支持的语言”)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OsZgov6P-1618134780220)(http://thyrsi.com/t6/644/1546068340x2890174094.jpg “中文”)]

来看一个简单中文例子

gherkin

# language: zh-CN
功能: 失败的登录

  场景大纲: 失败的登录
    假设 当我在网站的首页
    当 输入用户名 <用户名>
    当 输入密码 <密码>
    当 提交登录信息
    那么 页面应该返回 "Error Page"

    例子:
      |用户名     |密码      |
      |'Jan1'    |'password'|
      |'Jan2'    |'password'|

JavaScript对应实现方法

    var { Given, When, Then,And} = require('cucumber');

    Given('当我在网站的首页', function() {
        return this.driver.get('http://0.0.0.0:7272/');
    });

    When('输入用户名 {string}', function (text) {
        return this.driver.findElement(By.id('username_field')).sendKeys(text)
    });

    When('输入密码 {string}', function (text) {
        return this.driver.findElement(By.id('password_field')).sendKeys(text)
    });

    When('提交登录信息', function () {
        return this.driver.findElement(By.id('login_button')).click()
    });

    Then('页面应该返回 {string}', function (string) {
      this.driver.getTitle().then(function(title) {
        expect(title).to.equal(string);
      });
    });

如果你有写过java版cucumber,你会发现Java与JavaScript的区别,在于JavaScript只要在Gherkin feature文件中写中文:假设-当-那么,而JavaScript文件中,实现方法的Gherkin关键字还是英文Given-When-Then。

如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uNfMEY91-1618134780221)(http://thyrsi.com/t6/647/1546356083x2890211744.jpg)]

总结

行为驱动开发是确保产品功能运行正确的绝佳方式,而Cucumber正是一个实践这种测试理念的强大工具。使用Cucumber,可以使产品中的每个参与者都可以阅读,理解甚至编写测试。

这篇文章只是抓住了Cucumber功能的表面,所以我建议你自己尝试一下,去体验它的作用。Cucumber也有一个非常活跃的社区,他们的Google Group和Gitter频道是当你遇到困难时寻求帮助的好去处。

你已经开始使用Cucumber了吗?这篇文章是否鼓励了你进行尝试?如果你想支持我,可以点击底部Donate按钮,支持下!

推荐几个好用的调试工具

cuketest

cucumber debug

参考链接

cucumber 官方教程

cucumber for JavaScript 官方源码

cucumber for JavaScript 官方API

cucumber for ruby 官方源码

cucumber for Java 官方源码

cucumber for Java IMB教程

声明

转载请注明出处:https://liyinchigithub.github.io/ 谢谢您的配合

你可能感兴趣的:(功能测试,接口测试,自动化测试,cucumber,bdd,tdd,敏捷开发,scrum)