知識庫 / Testing RSS 訂閱

REST API 測試與 Cucumber

REST,Testing
HongKong
5
03:56 AM · Dec 06 ,2025

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子句可以被認為是用例的前提條件。
  • WhenWhen步驟用於描述應用程序中發生的事件。 這可以是用户採取的操作,或者由另一個系統觸發的事件。
  • Then: 此步驟用於指定測試的預期結果。 結果應與測試的特性業務價值相關。
  • 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 文本中的步驟轉換為可執行代碼。 在解析 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。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.