Contents

Cucumber 进行REST API测试

1. 概述

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

此外,为了使文章自包含并独立于任何外部 REST 服务,我们将使用 WireMock,一个存根和模拟 Web 服务库。如果你想了解更多关于这个库的信息,请参考WireMock 的介绍。

2. Cucumber——语言

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

2.1. 简介

Gherkin 是一种面向行的语言,使用行尾、缩进和关键字来定义文档。每个非空行通常以 Gherkin 关键字开头,后跟任意文本,通常是关键字的描述。

必须将整个结构写入具有Cucumber 识别的功能扩展名的文件中。

这是一个简单的 Gherkin 文档示例:

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

在接下来的部分中,我们将描述 Gherkin 结构中的几个最重要的元素。

2.2. 特征

我们使用 Gherkin 文件来描述需要测试的应用程序功能。该文件的开头包含Feature关键字,随后是同一行的功能名称以及可能跨越多行的可选描述。

Cucumber 解析器会跳过除Feature关键字之外的所有文本,并将其包含在文档中。

2.3. 场景和步骤

Gherkin 结构可能包含一个或多个场景,由Scenario关键字识别。场景基本上是允许用户验证应用程序功能的测试。它应该描述初始背景、可能发生的事件以及这些事件产生的预期结果。

这些事情是使用步骤完成的,由五个关键字之一标识:GivenWhenThenAndBut

  • Given:此步骤是在用户开始与应用程序交互之前将系统置于明确定义的状态。可以将Given子句视为用例的先决条件。
  • When : When步骤用于描述应用程序发生的事件。这可以是用户执行的操作,也可以是其他系统触发的事件。
  • 然后:这一步是指定测试的预期结果。结果应该与被测特性的业务价值相关。
  • AndBut:当有多个相同类型的步骤时,可以使用这些关键字替换上述步骤关键字。

Cucumber 实际上并没有区分这些关键字,但是它们仍然存在以使该功能更具可读性并与 BDD 结构保持一致。

3. Cucumber-JVM实现

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

3.1. Maven 依赖项

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

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

为了方便使用 Cucumber 进行 JUnit 测试,我们需要多一个依赖项:

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

或者,我们可以使用另一个工件来利用 Java 8 中的 lambda 表达式,本教程将不涉及。

3.2. 步骤定义

如果不将 Gherkin 场景转化为动作,那么它们将毫无用处,这就是步骤定义发挥作用的地方。基本上,步骤定义是带有附加模式的带注释的 Java 方法,其工作是将纯文本中的 Gherkin 步骤转换为可执行代码。解析功能文档后,Cucumber 将搜索与预定义 Gherkin 步骤匹配的步骤定义以执行。

为了更清楚,让我们看一下以下步骤:

Given I have registered a course in Blogdemo

和一个步骤定义:

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

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

4. 创建和运行测试

4.1. 编写功能文件

让我们从在名称以*.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

我们现在将此文件保存在名为Feature的目录中,条件是该目录将在运行时加载到类路径中,例如src/main/resources

4.2. 配置 JUnit 以使用 Cucumber

为了让 JUnit 在运行时了解 Cucumber 并读取功能文件,必须将Cucumber类声明为Runner。我们还需要告诉 JUnit 搜索功能文件和步骤定义的位置。

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

可以看到,CucumberOptionfeatures元素定位到之前创建的特征文件。另一个重要的元素,称为glue,提供了步骤定义的路径。但是,如果测试用例和步骤定义与本教程位于同一包中,则可能会删除该元素。

4.3. 编写步骤定义

当 Cucumber 解析步骤时,它会搜索带有 Gherkin 关键字注解的方法来定位匹配的步骤定义。

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

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

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

}

这是一个匹配 Gherkin 步骤并从文本中获取参数的方法,该参数将用于从 REST Web 服务获取信息:

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

}

如您所见,usersGetInformationOnAProject方法采用String参数,即项目名称。此参数由注释中的*{string}声明,在这里它对应于步骤文本中的Cucumber* 。

或者,我们可以使用正则表达式:

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

请注意,’^’’$’相应地指示正则表达式的开始和结束。而’(.+)’对应于String参数。

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

4.4. 创建和运行测试

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

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

为了演示 REST API,我们使用 WireMock 服务器:

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

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

CloseableHttpClient httpClient = HttpClients.createDefault();

现在,让我们继续在步骤定义中编写测试代码。我们将首先为usersUploadDataOnAProject方法执行此操作。 服务器应该在客户端连接到它之前运行:

wireMockServer.start();

使用 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)));

现在,向服务器发送一个 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);

以下代码断言 POST 请求已成功接收和处理:

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

服务器在使用后应该停止:

wireMockServer.stop();

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

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

提交 GET 请求并接收响应:

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

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

String responseString = convertResponseToString(httpResponse);

这是该转换辅助方法的实现:

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;
}

下面验证整个过程:

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

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

5. 并行运行特性

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

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

现在让我们添加插件配置:

<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>

注意:

  • parallel:可以是classmethods或两者兼有——在我们的例子中,class将使每个测试类在单独的线程中运行
  • threadCount:表示应该为此执行分配多少线程

这就是我们并行运行 Cucumber 功能所需要做的一切。