本教程介绍Cucumber——一个常用的用户验收测试工具,以及如何在REST API测试中使用它。为使文章自包含且不依赖外部REST服务,我们将使用WireMock(一个桩服务和模拟Web服务库)。想深入了解该库可参考WireMock入门指南。
2. Gherkin – Cucumber的语言
Cucumber是支持行为驱动开发(BDD)的测试框架,允许用户用纯文本定义应用操作。其核心是Gherkin领域特定语言(DSL)。这种简洁而强大的语法让开发者和测试人员能编写复杂测试,同时保持非技术人员的可读性。
2.1. Gherkin简介
Gherkin是一种行导向语言,通过行结束符、缩进和关键词定义文档。每个非空行通常以Gherkin关键词开头,后接任意描述文本。整个结构需写入扩展名为.feature
的文件才能被Cucumber识别。
简单示例:
Feature: 所需功能的简短描述
Scenario: 业务场景
Given 前置条件
And 另一个前置条件
When 事件发生
And 另一个事件也发生
Then 可验证的结果达成
And 其他事项也完成
2.2. 功能(Feature)
Gherkin文件用于描述需要测试的应用功能。文件以Feature
关键词开头,同行紧跟功能名称,下方可跨行添加可选描述。Cucumber解析器会跳过除Feature
外的所有文本,仅作为文档保留。
2.3. 场景与步骤(Scenarios and Steps)
Gherkin结构包含一个或多个场景(Scenario
关键词标识)。场景本质是测试,用于验证应用能力,需描述初始上下文、可能发生的事件及预期结果。通过步骤实现,步骤由五个关键词标识:
Given
:将系统置于用户交互前的明确定义状态,可视为用例前置条件When
:描述应用事件(用户操作或其他系统触发的事件)Then
:指定测试预期结果,结果应与被测功能的业务价值相关And
/But
:替换上述关键词,用于连接多个同类型步骤
Cucumber实际不区分这些关键词,但它们的存在提升了可读性和BDD结构一致性。
3. Cucumber-JVM实现
Cucumber最初用Ruby编写,后移植到Java形成Cucumber-JVM实现。
3.1. Maven依赖
在Maven项目中需添加以下依赖:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>6.8.0</version>
<scope>test</scope>
</dependency>
为支持JUnit测试,还需添加:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>6.8.0</version>
</dependency>
3.2. 步骤定义(Step Definitions)
Gherkin场景需转换为动作,这就是步骤定义的作用。步骤定义本质是带模式注解的Java方法,用于将纯文本Gherkin步骤转换为可执行代码。Cucumber解析功能文档后,会搜索匹配预定义Gherkin步骤的步骤定义执行。
示例: Gherkin步骤:
Given 我已在Baeldung注册课程
步骤定义:
@Given("我已在Baeldung注册课程")
public void verifyAccount() {
// 方法实现
}
当Cucumber读取该步骤时,会寻找注解模式匹配Gherkin文本的步骤定义。
4. 创建和运行测试
4.1. 编写功能文件
创建扩展名为.feature
的文件,声明场景和步骤:
Feature: 测试REST API
用户应能向WireMock表示的Web服务提交GET和POST请求
Scenario: 向Web服务上传数据
When 用户上传项目数据
Then 服务器应处理并返回成功状态
Scenario: 从Web服务检索数据
When 用户想获取'Cucumber'项目信息
Then 返回请求的数据
将文件保存在Feature
目录(需在运行时加载到类路径,如src/main/resources
)。
4.2. 配置JUnit与Cucumber协同工作
声明Cucumber
作为Runner
,并指定功能文件和步骤定义的搜索路径:
@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:Feature")
public class CucumberIntegrationTest {
}
features
元素定位功能文件glue
元素提供步骤定义路径(若测试用例和步骤定义在同一包中可省略)
4.3. 编写步骤定义
Cucumber解析步骤时,会搜索带Gherkin关键词注解的方法匹配步骤定义。步骤定义表达式可以是正则表达式或Cucumber表达式(本教程使用后者)。
匹配POST请求步骤的方法:
@When("用户上传项目数据")
public void usersUploadDataOnAProject() throws IOException {
}
带参数的GET请求步骤方法:
@When("用户想获取{string}项目信息")
public void usersGetInformationOnAProject(String projectName) throws IOException {
}
参数通过注解中的{string}
声明(对应步骤文本中的Cucumber
)。也可使用正则表达式:
@When("^用户想获取'(.+)'项目信息$")
public void usersGetInformationOnAProject(String projectName) throws IOException {
}
4.4. 创建和运行测试
数据结构示例(POST请求上传/GET请求下载):
{
"testing-framework": "cucumber",
"supported-language":
[
"Ruby",
"Java",
"Javascript",
"PHP",
"Python",
"C++"
],
"website": "cucumber.io"
}
初始化服务与客户端:
WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());
CloseableHttpClient httpClient = HttpClients.createDefault();
实现usersUploadDataOnAProject
方法:
@When("用户上传项目数据")
public void usersUploadDataOnAProject() throws IOException {
// 启动服务器
wireMockServer.start();
// 桩接REST服务
configureFor("localhost", wireMockServer.port());
stubFor(post(urlEqualTo("/create"))
.withHeader("content-type", equalTo("application/json"))
.withRequestBody(containing("testing-framework"))
.willReturn(aResponse().withStatus(200)));
// 发送POST请求
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);
// 验证响应
assertEquals(200, response.getStatusLine().getStatusCode());
verify(postRequestedFor(urlEqualTo("/create"))
.withHeader("content-type", equalTo("application/json")));
// 停止服务器
wireMockServer.stop();
}
实现usersGetInformationOnAProject
方法:
@When("用户想获取{string}项目信息")
public void usersGetInformationOnAProject(String projectName) throws IOException {
// 启动服务器
wireMockServer.start();
// 桩接REST服务
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);
// 转换响应为字符串
String responseString = convertResponseToString(httpResponse);
// 验证响应
assertThat(responseString, containsString("\"testing-framework\": \"cucumber\""));
assertThat(responseString, containsString("\"website\": \"cucumber.io\""));
verify(getRequestedFor(urlEqualTo("/projects/cucumber"))
.withHeader("accept", equalTo("application/json")));
// 停止服务器
wireMockServer.stop();
}
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;
}
5. 并行运行功能
Cucumber-JVM原生支持多线程并行测试。使用JUnit配合Maven Failsafe插件执行运行器(也可用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
:可选classes
/methods
/both
(classes
使每个测试类在独立线程运行)threadCount
:指定分配线程数
6. 总结
本教程介绍了Cucumber基础,以及该框架如何使用Gherkin领域特定语言测试REST API。所有代码示例可在GitHub获取。