[Java] BDD 測試框架 Cucumber 入門範例教學

Cucumber 是一個支援 BDD (Behaviour-Driven Development) 行為的自動化測試框架,支援多種常見的實作語言,包含 Java、Node.js、Go、Ruby 等。

關於 Cucumber 的入門介紹和基本術語可以參考以下文章:

本文示範如何在 Java 使用 Cucumber。

範例題目說明 —— TGIF

TGIF 指的是 Thank God It’s Friday!

TGIF 是 Cucumber 官方教學使用的範例題目,我覺得很有梗,所以本文教學沿用這個主題。但個人覺得官方範例為了簡化困難度,把範例邏輯改得不貼近實務,反而有點沒感覺,所以本文的範例內容會改良。

範例目標很簡單,就是實作一個功能,告訴我今天是不是星期五。

需求條列來說:

  • 接受傳入一個日期
  • 如果不是星期五,回傳「Nope」字串
  • 如果是星期五,回傳「TGIF」字串

Prerequisites (前置工作)

環境準備:

  • Java SE: 建議版本 8+
  • Build tool: Maven 3.3.1+ 或 Gradle
  • IDE: 任何你喜歡的編輯器,例如 IntelliJ IDEA 或 Eclipse

以下的示範是使用 Maven 和 IntelliJ IDEA。

1.建立一個全新 Cucumber 專案

Step 1-1: 使用 Maven plugin cucumber-archetype 建立專案

這裏使用 Cucumber 的 Maven archetype 幫忙建立專案:

$ mvn archetype:generate                           \
   -DarchetypeGroupId=io.cucumber                  \
   -DarchetypeArtifactId=cucumber-archetype        \
   -DarchetypeVersion=4.2.6.1                      \
   -DgroupId=com.onejar99                          \
   -DartifactId=HelloCucumberJava                  \
   -Dpackage=com.onejar99.hellocucumberjava        \
   -Dversion=1.0.0-SNAPSHOT                        \
   -DinteractiveMode=false

執行以上指令後會自動建立一個名為「HelloCucumberJava」的專案資料夾,並幫忙建立相關的資料夾結構和預設檔案:

$ tree HelloCucumberJava/
HelloCucumberJava/
├── pom.xml
└── src
    └── test
        ├── java
        │   └── com
        │       └── onejar99
        │           └── hellocucumberjava
        │               ├── RunCucumberTest.java
        │               └── Stepdefs.java
        └── resources
            └── com
                └── onejar99
                    └── hellocucumberjava

10 directories, 3 files

如果檢視 pom.xml 檔,可以看到已經安裝幾個自動化測試需要的 package,包含 JUnit、cucumber-junit、cucumber-java:

    <dependencies>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Step 1-2: 驗證 Cucumber 安裝成功

在開始寫測試前,可以先快速驗證一下 Cucumber 是否安裝成功:

$ cd HelloCucumberJava/
$ mvn test

如果看到 BUILD SUCCESS,就表示 Cucumber 安裝成功、運作正常:

Cucumber 執行完畢後,會告訴你執行了幾個測試步驟、幾個 Failed、有多少案例跳過等資訊。因為目前還沒寫任何測試,所以統計數字都是 0。

2. 撰寫測試案例

接著就可以打開編輯器開始寫測試案例的內容。

Step 2-1: 撰寫 Scenarios (.feature)

我們準備增加兩個測試案例:

  • 一個是驗證 2020-01-01 不是星期五,回傳 Nope 字串
  • 一個是驗證 2020-01-03 是星期五,回傳 TGIF 字串

新增一個 feature 檔:

$ vi src/test/resources/com/onejar99/hellocucumberjava/is_it_friday_yet.feature
Feature: Is it Friday yet?
  Everybody wants to know when it's Friday

  Scenario: 2020-01-01 isn't Friday
    Given today is Year 2020, Month 1, Day 1
    When I ask whether it's Friday yet
    Then I should be told "Nope"

  Scenario: 2020-01-03 is Friday
    Given today is Year 2020, Month 1, Day 3
    When I ask whether it's Friday yet
    Then I should be told "TGIF"

如果你的 IDE 支援 Cucumber 語法,可能在編輯器上會看到一些方便的提示。例如下面是 IntelliJ IDEA 的介面,每個 Step 描述都有一層反灰,提醒 Step 還沒被定義:

這時候執行 mvn test,會看到執行結果還是 BUILD SUCCESS,但 Cucumber 會提示你有哪些 Step 因為還沒定義而被跳過沒執行:

甚至直接提供 Step 定義的程式碼範本:

Step 2-2: 撰寫 Step 定義 —— 貼上程式碼範本

我們試著把 Cucumber 提供的程式碼範本直接貼到專案裡:

$ vi src/test/java/com/onejar99/hellocucumberjava/Stepdefs.java
package com.onejar99.hellocucumberjava;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;

import static org.junit.Assert.*;

public class Stepdefs {
    @Given("today is Year {int}, Month {int}, Day {int}")
    public void today_is_Year_Month_Day(Integer int1, Integer int2, Integer int3) {
        // Write code here that turns the phrase above into concrete actions
        throw new cucumber.api.PendingException();
    }

    @When("I ask whether it's Friday yet")
    public void i_ask_whether_it_s_Friday_yet() {
        // Write code here that turns the phrase above into concrete actions
        throw new cucumber.api.PendingException();
    }

    @Then("I should be told {string}")
    public void i_should_be_told(String string) {
        // Write code here that turns the phrase above into concrete actions
        throw new cucumber.api.PendingException();
    }
}

執行 mvn test,結果訊息稍有不同。因為 Step 已經定義但拋出 PendingException,Cucumber 會知道這個 Step 是「TODO」項目,在執行結果裡提醒你,並幫你跳過該 Scenarios 後續的 Step:

Step 2-3: 撰寫完整的 Step 定義

接著根據題目需求,完成真正的 Step 定義內容。直接基於程式碼範本繼續修改,修改重點:

  • 定義每個 Step 的動作內容。
  • 為了讓 Step 可以真的呼叫,我們會在產品程式新增一個 TGIFUtil 類別,定義好 API 介面,但還不需要實作 API 內容,這個階段專注在完成測試程式即可。
  • 優化:重新命名 Step 參數名稱,提升可讀性,例如原本的 int1, int2, int3, string 改成 year, month, day, answer
  • 優化:將原本 Step Annotation 的參數化代號如 {string}{int} 優化成正規表示法的寫法。
$ vi src/test/java/com/onejar99/hellocucumberjava/Stepdefs.java
package com.onejar99.hellocucumberjava;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;

import java.time.LocalDate;

import static org.junit.Assert.*;

public class Stepdefs {
    private LocalDate today;
    private String answer;

    @Given("^today is Year (\\d+), Month (\\d+), Day (\\d+)$")
    public void today_is_Year_Month_Day(Integer year, Integer month, Integer day) {
        this.today = LocalDate.of(year, month, day);
    }

    @When("^I ask whether it's Friday yet$")
    public void i_ask_whether_it_s_Friday_yet() {
        this.answer = TGIFUtil.isItFriday(this.today);
    }

    @Then("^I should be told \"([^\"]*)\"$")
    public void i_should_be_told(String expectedAnswer) {
        assertEquals(this.answer, expectedAnswer);
    }
}
$ vi src/main/java/com/onejar99/hellocucumberjava/TGIFUtil.java
package com.onejar99.hellocucumberjava;

import java.time.LocalDate;

public class TGIFUtil {
    public static String isItFriday(LocalDate date) {
        // TODO
        return null;
    }
}

執行 mvn test,會看到測試結果在驗證的 Step 「Then I should be told "OOO"」發生 Failed,這是因為我們還沒實作 TGIFUtil 的 API 內容,所以得到錯誤回傳,測試結果自然是 Failed。

但同時代表 Cucumber 自動化測試程式的部分已經就緒,只差真正的產品程式實作,這就是 TDD (Test-Driven Development) 裡的「紅燈測試」階段

Step 2-4: 實作產品程式

到這裡只差一步,就是實作 TGIFUtil 裡的內容:

$ vi src/main/java/com/onejar99/hellocucumberjava/TGIFUtil.java
package com.onejar99.hellocucumberjava;

import java.time.DayOfWeek;
import java.time.LocalDate;

public class TGIFUtil {
    public static String isItFriday(LocalDate date) {
        DayOfWeek dayOfWeek = date.getDayOfWeek();
        return dayOfWeek == DayOfWeek.FRIDAY ? "TGIF" : "Nope";
    }
}

執行 mvn test,應該要看到全部測試都 Passed,也就是 TDD 中提到的「綠燈測試」 (除非你程式有 Bug,幫你發現 Bug 就是自動化測試的目的):

3. 優化:使用 Scenario Outline 參數化 Scenario

如果想多測幾個情境,依照上面的寫法,就是在 .feature 檔裡繼續增加 Scenario。

但每個 Scenario 的流程其實差不多,只是給予的日期、預期回傳的字串答案不同,如果每多測試一個日期就增加一個 Scenario 會讓原始碼很繁瑣累贅。

因應這種情況,Cucumber 提供了「Scenario Outline」的寫法,讓同樣的 Scenario 流程可以套用不同參數

Scenario Outline 寫法要點:

  1. 原本的 Scenario: 改成 Scenario Outline:
  2. 將要參數化的地方用 <OOO> 表示,例如 <year>, <month>, <day>, <answer>
  3. 增加一個 Examples 列表,將 <year>, <month>, <day>, <answer> 設定多組想測試的值。

如此就能很輕易的為同一個 Scenario 新增不同的測試案例。

$ vi src/test/resources/com/onejar99/hellocucumberjava/is_it_friday_yet.feature
Feature: Is it Friday yet?
  Everybody wants to know when it's Friday

  Scenario Outline: Today is or is not Friday
    Given today is Year <year>, Month <month>, Day <day>
    When I ask whether it's Friday yet
    Then I should be told "<answer>"

    Examples:
      | year | month | day | answer |
      | 2020 | 1     | 1   | Nope   |
      | 2020 | 1     | 3   | TGIF   |
      | 2019 | 9     | 6   | TGIF   |
      | 2019 | 9     | 7   | Nope   |

執行 mvn test

結語

本文示範如何用 Java 實作簡單的 Cucumber 範例,順便走了一次 TDD 的紅燈、綠燈流程。Cucumber 還有更多的使用細節和技巧,可以參考官方的教學文件。

References

發表留言