REST API Testing with Cucumber – 用Cucumber测试REST API

最后修改: 2016年 6月 8日

1. Overview

1.概述

This tutorial gives an introduction to Cucumber, a commonly used tool for user acceptance testing, and how to use it in REST API tests.

本教程介绍了Cucumber,这是一种常用的用户验收测试工具,以及如何在REST API测试中使用它。

In addition, to make the article self-contained and independent of any external REST services, we will use WireMock, a stubbing and mocking web service library. If you want to know more about this library, please refer to the introduction to WireMock.

此外,为了使文章自成一体并独立于任何外部REST服务,我们将使用WireMock,一个存根和嘲弄的Web服务库。如果你想进一步了解这个库,请参考WireMock的介绍

2. Gherkin – the Language of Cucumber

2.小黄瓜–黄瓜的语言

Cucumber is a testing framework that supports Behavior Driven Development (BDD), allowing users to define application operations in plain text. It works based on the Gherkin Domain Specific Language (DSL). This simple but powerful syntax of Gherkin lets developers and testers write complex tests while keeping it comprehensible to even non-technical users.

Cucumber是一个测试框架,它支持行为驱动开发(BDD),允许用户用纯文本定义应用程序操作。它基于Gherkin领域特定语言(DSL)工作。Gherkin的这种简单而强大的语法使开发人员和测试人员能够编写复杂的测试,同时使非技术用户也能理解它。

2.1. Introduction to Gherkin

2.1.小黄瓜简介

Gherkin is a line-oriented language using line endings, indentations and keywords to define documents. Each non-blank line usually starts with a Gherkin keyword, followed by an arbitrary text, which is usually a description of the keyword.

小黄瓜是一种面向行的语言,使用行尾、缩进和关键词来定义文档。每一个非空白行通常以一个小黄瓜关键词开始,后面是一个任意的文本,通常是对该关键词的描述。

The whole structure must be written into a file with the feature extension to be recognized by Cucumber.

整个结构必须写进一个带有feature扩展名的文件,以便被Cucumber识别。

Here is a simple Gherkin document example:

下面是一个简单的小黄瓜文件的例子。

Feature: A short description of the desired functionality

  Scenario: A business situation
    Given a precondition
    And another precondition
    When an event happens
    And another event happens too
    Then a testable outcome is achieved
    And something else is also completed

In the following sections, we’ll describe a couple of the most important elements in a Gherkin structure.

在下面的章节中,我们将描述小黄瓜结构中几个最重要的元素。

2.2. Feature

2.2.特点

We use a Gherkin file to describe an application feature that needs to be tested. The file contains the Feature keyword at the very beginning, followed up by the feature name on the same line and an optional description that may span multiple lines underneath.

我们使用一个小黄瓜文件来描述需要测试的应用程序特性。该文件在最开始包含Feature关键字,在同一行中包含功能名称和可选的描述,描述可能跨越多行。

Cucumber parser skips all the text, except for the Feature keyword, and includes it for the purpose of documentation only.

Cucumber解析器跳过了所有的文本,除了Feature关键字,包括它的目的只是为了文档。

2.3. Scenarios and Steps

2.3.情景和步骤

A Gherkin structure may consist of one or more scenarios, recognized by the Scenario keyword. A scenario is basically a test allowing users to validate a capability of the application. It should describe an initial context, events that may happen and expected outcomes created by those events.

一个小黄瓜结构可以由一个或多个场景组成,由Scenario关键字识别。一个场景基本上是一个测试,允许用户验证应用程序的能力。它应该描述一个初始环境、可能发生的事件和这些事件产生的预期结果。

These things are done using steps, identified by one of the five keywords: Given, When, Then, And, and But.

这些事情是用步骤完成的,由五个关键词之一来确定。Given, When, Then, And, 和But

  • Given: This step is to put the system into a well-defined state before users start interacting with the application. A Given clause can by considered a precondition for the use case.
  • When: A When step is used to describe an event that happens to the application. This can be an action taken by users, or an event triggered by another system.
  • Then: This step is to specify an expected outcome of the test. The outcome should be related to business values of the feature under test.
  • And and But: These keywords can be used to replace the above step keywords when there are multiple steps of the same type.

Cucumber does not actually distinguish these keywords, however they are still there to make the feature more readable and consistent with the BDD structure.

Cucumber实际上并不区分这些关键词,然而它们仍然存在,以使功能更易读,并与BDD结构保持一致。

3. Cucumber-JVM Implementation

3.Cucumber-JVM的实现

Cucumber was originally written in Ruby and has been ported into Java with Cucumber-JVM implementation, which is the subject of this section.

Cucumber最初是用Ruby编写的,并通过Cucumber-JVM实现移植到了Java中,这也是本节的主题。

3.1. Maven Dependencies

3.1.Maven的依赖性

In order to make use of Cucumber-JVM in a Maven project, the following dependency needs to be included in the POM:

为了在Maven项目中使用Cucumber-JVM,需要在POM中包含以下依赖关系。

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>6.8.0</version>
    <scope>test</scope>
</dependency>

To facilitate JUnit testing with Cucumber, we need to have one more dependency:

为了方便用Cucumber进行JUnit测试,我们需要多一个依赖关系。

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit</artifactId>
    <version>6.8.0</version>
</dependency>

Alternatively, we can use another artifact to take advantage of lambda expressions in Java 8, which won’t be covered in this tutorial.

另外,我们还可以使用另一个神器来利用Java 8中的lambda表达式,本教程中不会涉及。

3.2. Step Definitions

3.2.步骤的定义

Gherkin scenarios would be useless if they were not translated into actions and this is where step definitions come into play. Basically, a step definition is an annotated Java method with an attached pattern whose job is to convert Gherkin steps in plain text to executable code. After parsing a feature document, Cucumber will search for step definitions that match predefined Gherkin steps to execute.

如果Gherkin场景不被转化为行动,那么它们将毫无用处,这就是步骤定义发挥作用的地方。基本上,步骤定义是一个带有注释的Java方法,它的工作是将纯文本中的Gherkin步骤转换为可执行代码。在解析特征文档后,Cucumber会搜索与预定义的Gherkin步骤相匹配的步骤定义来执行。

In order to make it clearer, let’s take a look at the following step:

为了更清楚地说明问题,让我们看看下面的步骤。

Given I have registered a course in Baeldung

And a step definition:

还有一个步骤的定义。

@Given("I have registered a course in Baeldung")
public void verifyAccount() {
    // method implementation
}

When Cucumber reads the given step, it will be looking for step definitions whose annotating patterns match the Gherkin text.

当Cucumber读取给定的步骤时,它将寻找其注释模式与小黄瓜文本相匹配的步骤定义。

4. Creating and Running Tests

4.创建和运行测试

4.1. Writing a Feature File

4.1.编写特征文件

Let’s start with declaring scenarios and steps in a file with the name ending in the .feature extension:

让我们先在一个名字以.feature扩展名结尾的文件中声明场景和步骤。

Feature: Testing a REST API
  Users should be able to submit GET and POST requests to a web service, 
  represented by WireMock

  Scenario: Data Upload to a web service
    When users upload data on a project
    Then the server should handle it and return a success status

  Scenario: Data retrieval from a web service
    When users want to get information on the 'Cucumber' project
    Then the requested data is returned

We now save this file in a directory named Feature, on the condition that the directory will be loaded into the classpath at runtime, e.g. src/main/resources.

我们现在把这个文件保存在一个名为Feature的目录中,条件是这个目录将在运行时被加载到classpath中,例如src/main/resources

4.2. Configuring JUnit to Work With Cucumber

4.2.配置JUnit以便与Cucumber一起工作

In order for JUnit to be aware of Cucumber and read feature files when running, the Cucumber class must be declared as the Runner. We also need to tell JUnit the place to search for feature files and step definitions.

为了让JUnit知道Cucumber并在运行时读取特征文件,Cucumber类必须被声明为Runner。我们还需要告诉JUnit搜索特征文件和步骤定义的地方。

@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:Feature")
public class CucumberIntegrationTest {
    
}

As you can see, the features element of CucumberOption locates the feature file created before. Another important element, called glue, provides paths to step definitions. However, if the test case and step definitions are in the same package as in this tutorial, that element may be dropped.

如你所见,CucumberOptionfeatures元素定位了之前创建的特征文件。另一个重要的元素,称为glue,提供步骤定义的路径。但是,如果测试用例和步骤定义与本教程中的测试用例在同一个包中,则该元素可以被放弃。

4.3. Writing Step Definitions

4.3.编写步骤定义

When Cucumber parses steps, it will search for methods annotated with Gherkin keywords to locate the matching step definitions.

当Cucumber解析步骤时,它将搜索带有Gherkin关键字注释的方法,以找到匹配的步骤定义。

A step definition’s expression can either be a Regular Expression or a Cucumber Expression. In this tutorial, we’ll use Cucumber Expressions.

一个步骤定义的表达式可以是正则表达式,也可以是Cucumber表达式。在本教程中,我们将使用Cucumber表达式。

The following is a method that fully matches a Gherkin step. The method will be used to post data to a REST web service:

下面是一个完全匹配Gherkin步骤的方法。该方法将用于将数据发布到一个REST网络服务。

@When("users upload data on a project")
public void usersUploadDataOnAProject() throws IOException {
    
}

And here is a method matching a Gherkin step and takes an argument from the text, which will be used to get information from a REST web service:

而这里是一个匹配小黄瓜步骤的方法,并从文本中获取一个参数,这将被用来从REST网络服务中获取信息。

@When("users want to get information on the {string} project")
public void usersGetInformationOnAProject(String projectName) throws IOException {
    
}

As you can see, the usersGetInformationOnAProject method takes a String argument, which is the project name. This argument is declared by {string} in the annotation and over here it corresponds to Cucumber in the step text.

正如你所看到的,usersGetInformationOnAProject方法需要一个String参数,也就是项目名称。这个参数在注解中用{string}声明,在这里它对应于步骤文本中的Cucumber

Alternatively, we could use a regular expression:

另外,我们也可以使用正则表达式。

@When("^users want to get information on the '(.+)' project$")
public void usersGetInformationOnAProject(String projectName) throws IOException {
    
}

Note, the ‘^’ and ‘$’ which indicate the start and end of the regex accordingly. Whereas ‘(.+)’ corresponds to the String parameter.

注意,‘^’‘$’相应地表示了重码的开始和结束。而‘(.+)’/em>则对应于String参数。

We’ll provide the working code for both of the above methods in the next section.

我们将在下一节提供上述两种方法的工作代码。

4.4. Creating and Running Tests

4.4.创建和运行测试</strong

First, we will begin with a JSON structure to illustrate the data uploaded to the server by a POST request, and downloaded to the client using a GET. This structure is saved in the jsonString field, and shown below:

首先,我们将从一个JSON结构开始,说明通过POST请求上传到服务器,并通过GET下载到客户端的数据。这个结构被保存在jsonString字段中,如下所示。

{
    "testing-framework": "cucumber",
    "supported-language": 
    [
        "Ruby",
        "Java",
        "Javascript",
        "PHP",
        "Python",
        "C++"
    ],

    "website": "cucumber.io"
}

To demonstrate a REST API, we use a WireMock server:

为了演示一个REST API,我们使用WireMock服务器。

WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());

In addition, we’ll use Apache HttpClient API to represent the client used to connect to the server:

此外,我们将使用Apache HttpClient API来表示用于连接到服务器的客户端。

CloseableHttpClient httpClient = HttpClients.createDefault();

Now, let’s move on to writing testing code within step definitions. We will do this for the usersUploadDataOnAProject method first.

现在,让我们继续在步骤定义中编写测试代码。我们将首先对usersUploadDataOnAProject方法进行测试。

The server should be running before the client connects to it:

在客户端连接到服务器之前,服务器应该正在运行。

wireMockServer.start();

Using the WireMock API to stub the REST service:

使用WireMock API来存根REST服务。

configureFor("localhost", wireMockServer.port());
stubFor(post(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json"))
  .withRequestBody(containing("testing-framework"))
  .willReturn(aResponse().withStatus(200)));

Now, send a POST request with the content taken from the jsonString field declared above to the server:

现在,向服务器发送一个POST请求,内容取自上面声明的jsonString字段。

HttpPost request = new HttpPost("http://localhost:" + wireMockServer.port() + "/create");
StringEntity entity = new StringEntity(jsonString);
request.addHeader("content-type", "application/json");
request.setEntity(entity);
HttpResponse response = httpClient.execute(request);

The following code asserts that the POST request has been successfully received and handled:

下面的代码断言,POST请求已被成功接收和处理。

assertEquals(200, response.getStatusLine().getStatusCode());
verify(postRequestedFor(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json")));

The server should stop after being used:

服务器在被使用后应该停止。

wireMockServer.stop();

The second method we will implement herein is usersGetInformationOnAProject(String projectName). Similar to the first test, we need to start the server and then stub the REST service:

我们将在此实现的第二个方法是usersGetInformationOnAProject(String projectName)。与第一个测试类似,我们需要启动服务器,然后存根REST服务。

wireMockServer.start();

configureFor("localhost", wireMockServer.port());
stubFor(get(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json"))
  .willReturn(aResponse().withBody(jsonString)));

Submitting a GET request and receiving a response:

提交一个GET请求并收到一个响应。

HttpGet request = new HttpGet("http://localhost:" + wireMockServer.port() + "/projects/" + projectName.toLowerCase());
request.addHeader("accept", "application/json");
HttpResponse httpResponse = httpClient.execute(request);

We will convert the httpResponse variable to a String using a helper method:

我们将使用一个辅助方法把httpResponse变量转换为String

String responseString = convertResponseToString(httpResponse);

Here is the implementation of that conversion helper method:

下面是该转换辅助方法的实现。

private String convertResponseToString(HttpResponse response) throws IOException {
    InputStream responseStream = response.getEntity().getContent();
    Scanner scanner = new Scanner(responseStream, "UTF-8");
    String responseString = scanner.useDelimiter("\\Z").next();
    scanner.close();
    return responseString;
}

The following verifies the whole process:

以下是对整个过程的验证。

assertThat(responseString, containsString("\"testing-framework\": \"cucumber\""));
assertThat(responseString, containsString("\"website\": \"cucumber.io\""));
verify(getRequestedFor(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json")));

Finally, stop the server as described before.

最后,如前所述,停止服务器。

5. Running Features in Parallel

5.并行运行功能

Cucumber-JVM natively supports parallel test execution across multiple threads. We’ll use JUnit together with Maven Failsafe plugin to execute the runners. Alternatively, we could use Maven Surefire.

Cucumber-JVM原生支持多线程的并行测试执行。我们将使用JUnit和Maven Failsafe插件来执行运行器。或者,我们也可以使用Maven Surefire。

JUnit runs the feature files in parallel rather than scenarios, which means all the scenarios in a feature file will be executed by the same thread.

JUnit并行运行特征文件而不是场景,这意味着一个特征文件中的所有场景将由同一个线程执行

Let’s now add the plugin configuration:

现在让我们添加插件的配置。

<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
        <includes>
            <include>CucumberIntegrationTest.java</include>
        </includes>
        <parallel>methods</parallel>
        <threadCount>2</threadCount>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Note that:

请注意,。

  • parallel: can be classes, methods, or both – in our case, classes will make each test class run in a separate thread
  • threadCount: indicates how many threads should be allocated for this execution

That’s all we need to do to run the Cucumber features in parallel.

这就是我们需要做的,以并行方式运行Cucumber功能。

6. Conclusion

6.结论

In this tutorial, we covered the basics of Cucumber and how this framework uses the Gherkin domain-specific language for testing a REST API.

在本教程中,我们介绍了Cucumber的基础知识,以及这个框架如何使用Gherkin领域特定语言来测试REST API。

As usual, all code samples shown in this tutorial are available over on GitHub.

像往常一样,本教程中显示的所有代码样本都可以在GitHub上找到