1. 概述
本教程介紹 Cucumber,這是一種常用的用户驗收測試工具,以及如何將其應用於 REST API 測試。
此外,為了使文章具有自包含性和獨立性,不受任何外部 REST 服務的影響,我們將使用 WireMock,一個樁化和模擬 Web 服務庫。 如果您想了解更多關於該庫的信息,請參考 WireMock 的介紹。
2. Gherkin – 行為驅動開發(BDD)的語言
Cucumber 是一個支持行為驅動開發(BDD)的測試框架,允許用户在純文本中定義應用程序的操作。它基於 Gherkin 領域特定語言 (DSL)。Gherkin 這種簡潔而強大的語法,讓開發者和測試人員能夠編寫複雜的測試,同時也能讓非技術人員輕鬆理解。
2.1 介紹 Gherkin
Gherkin 是一種基於行的語言,使用行尾、縮進和關鍵字來定義文檔。每一行通常以 Gherkin 關鍵字開頭,後跟任意文本,通常是對關鍵字的描述。
整個結構必須寫入具有 feature 擴展名的文件,以便被 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關鍵字識別。 場景本質上是一種測試,允許用户驗證應用程序的能力。 它應該描述初始上下文、可能發生的事件以及由這些事件產生的預期結果。
這些操作使用步驟完成,這些步驟由以下五個關鍵字之一識別:Given, When, Then, And, 和 But。
- Given: 此步驟是為用户與應用程序交互之前,將系統置於明確的狀態。 一個Given子句可以被認為是用例的前提條件。
- When: When步驟用於描述應用程序中發生的事件。 這可以是用户採取的操作,或者由另一個系統觸發的事件。
- Then: 此步驟用於指定測試的預期結果。 結果應與測試的特性業務價值相關。
- And 和 But: 這些關鍵字可用於替換上述步驟關鍵字,當有相同類型的多個步驟時。
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 文本中的步驟轉換為可執行代碼。 在解析 feature 文檔後,Cucumber 將搜索與預定義的 Gherkin 步驟匹配的步驟定義,以便執行。
為了更清楚地説明,讓我們看一下下面的步驟:
Given I have registered a course in Baeldung以及一個步驟定義:
@Given("I have registered a course in Baeldung")
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 的目錄下,同時要求該目錄在運行時加載到 classpath 中,例如 src/main/resources。
4.2. 配置 JUnit 與 Cucumber 協同工作
為了使 JUnit 能夠感知 Cucumber 並讀取 feature 文件進行運行,必須將 Cucumber 類聲明為 Runner。 此外,還需要告知 JUnit 搜索 feature 文件和步驟定義的位置。
@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:Feature")
public class CucumberIntegrationTest {
}如您所見,features 元素位於 CucumberOption 中,用於定位在之前創建的特徵文件。 另一個重要的元素,稱為 glue,提供指向步驟定義的路徑。 但是,如果測試用例和步驟定義與本教程中相同的包中,則該元素可能會被刪除。
4.3. 編寫步驟定義 (Step Definitions)
當 Cucumber 解析步驟時,它會搜索帶有 Gherkin 關鍵詞的註解的方法,以查找匹配的步驟定義。
步驟定義的表達式可以是正則表達式 (Regular Expression) 或 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 步驟文本中的 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)));現在,將從 jsonString 字段中提取的內容作為內容,通過 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);以下代碼斷言 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>請注意:
- 並行: 可以是 類、方法,或兩者兼有 – 在我們的情況下,類 將使每個測試類在單獨的線程中運行
- 線程數: 指為本次執行應分配的線程數
這就是運行 Cucumber 特性所需的全部內容。
6. 結論
在本教程中,我們介紹了 Cucumber 的基本知識以及該框架如何使用 Gherkin 領域特定語言來測試 REST API。