透過 Cucumber 學習 BDD

在需求的開發過程中,最令人困惑的地方就在於需求模糊。需求是解決業務的問題,那麼驗收的方式應該是由業務方提出,但是往往業務方(可能是產品經理,也可能是直接是客戶)只能給出比較模糊的一個驗收標準,而程式卻是需要非常明確的輸入輸出的條件的。

這中間的鴻溝是否能夠通過一些手段來減輕(個人認為是無法完全消除的,資訊在傳遞的過程中一定會經歷一些損耗),Cucumber 就是一個為此提出的實例化需求框架。從這個框架提供的思路在於讓業務方提供明確的場景,讓開發為場景提供數據進行模擬,通過Cucumber進行銜接。

Cucumber 本身的定位是一個 BDD(Behavior Driven Development,行為驅動開發) 的測試框架。BDD是一種敏捷軟體開發的技術,它鼓勵軟體項目中的開發者、QA和非技術人員或商業參與者之間的協作。BDD側重設計,要求大家在設計測試用例的時候對系統進行定義,使用通用的語言將系統的行為描述出來,將系統設計和測試用例結合起來,從而以此為驅動進行開發工作。

假設我需要投票系統,在做出一系列分析和設計之後,得到這樣的需求:

##### 目標

為方便收集大家的回饋和活動構建一個`投票系統`

##### 投票功能設計

【使用場景】

1. 每一人`每輪`只能投出指定的有限的`票數`
2. 可以根據配置展示`每輪`的`投票結果`
   - 總是展示`投票結果`
   - `每輪`投票完成之後才展示`投票結果`

【管理場景】

... ...

【編輯場景】

... ...

在設計這個功能的時候,我們已經需要開始注意需求中使用的辭彙了,否則大家交流的功能的時候,可能就會發生歧義。在定義完這些需求之後,我們需要將這些需求沉澱到項目中。

Cucumber 使用了一個Gherkin的標記語言,有點像是YAML,的,Cucumber 解釋器可以通過理解使用Gherkin編寫的*.feature文件來執行對應的測試文件。採用Gherkin有兩個目的:

  1. 實例化需求,澄清驗收標準
  2. 自動化測試。

通過Gherkin形成業務方能閱讀理解的文件形式。這裡我們挑中投票使用場景中的一個功能編寫一個例子:

@投票功能
Feature: 【投票-使用場景】投票功能

  每一人`每輪`只能投出指定的有限的`票數`
  
  Scenario Outline: 每位`參與人``每輪`只能投出指定的有限的`票數`
    Given 發起一輪投票,設置`選項`
      | A | B | C |
    Given <recipientNumber>個`參與人`,每人投票<voteTimes>次
    When 所有`參與人`完成投票
    Then `投票結果`中總票數應該為<totalVotes>票

    Examples: 正常情況的用例
      | recipientNumber | voteTimes | totalVotes |
      | 2               | 1         | 2          |
      | 3               | 2         | 6          |

    Examples: 異常情況下的用例
      | recipientNumber | voteTimes | totalVotes |
      | 4               | 2         | 4          |

這裡將Feature中定義出這個功能。接著在下面寫出這個功能對應的場景Scenario。然後通過GivenWhenThen等關鍵字定義這個場景中的步驟。然後在下方通過Example關鍵字將用例中關鍵的資訊用表格的形式列出。

通過以上這些關鍵字加上對場景的描述,最終將需求落到的項目中,假如希望語言閱讀更加連貫,這些關鍵字也有對應的中文關鍵字支援編寫。

Gherkin的語法在網站上有比較詳細的描述,關於它的語法可以參考://cucumber.io/docs/gherkin/reference/

網上找的語法關鍵字收集

編寫這樣的一個feature文件之後可以將它放在test目錄的resources文件夾下:

.
├── java
│   └── io.github.whthomas.demo.cucumber
└── resources
    ├── cucumber-reporting.properties
    ├── cucumber.properties
    └── io.github.whthomas.demo.cucumber.vote
        ├── displayVoteResultScenario.feature
        └── useVoteScenario.feature

在沒有 Java 程式碼的情況之下,編寫一個RunCucumberTest類:

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty"})
public class RunCucumberTest {
}

運行RunCucumberTest,會給出如下提示,告知我們要怎麼做:

控制台會提示怎麼編寫程式碼

建立和 feature 文件同級的test目錄的src目錄,將這些提示程式碼拷貝到這個同package的類中(新建 Java 類,名稱可以根據需求和場景來取名),Java 類中就是一些驅動測試場景的程式碼了。Cucumber會根據Feature文件中的描述的步驟,逐一執行Java中的程式碼,並將參數傳遞進去。

這個時候目錄結構應該這些構建:

.
├── java
│   └── io.github.whthomas.demo.cucumber
│       ├── RunCucumberTest.java
│       └── vote
│          ├── DisplayVoteResultScenario.java
│          └── UseVoteScenario.java
└── resources
    ├── cucumber-reporting.properties
    ├── cucumber.properties
    └── io.github.whthomas.demo.cucumber
        └── vote
            ├── displayVoteResultScenario.feature
            └── useVoteScenario.feature

每次去執行RunCucumberTest的時候,就會在 IDE 中像展示文檔一樣,將需求和用例結合展示出來。

提示每個用例執行的結果

Cucumber也提供了一些第三方插件,將結果通過報告展示出來:

Cucumber Report

但是我覺得展示的效果,還是太偏向於是給技術人員閱讀的了。如果我們力求一份好的文檔能讓業務人員也能很好理解,可能還是需要利用它產生的json文件自己來開發一下才行。

現在在回過頭來看BDD的這個套路,把這整個套路整理一遍,這一切看起來還挺美好的:

  1. 需求分析和定義,整理需求
  2. 設計產品的功能,制定驗收標準。
  3. 編寫Gherkin文件,將需求實例化到文檔中,同時編寫業務用例。
  4. 構建測試程式碼,測試業務程式碼。
  5. 長期持續集成/持續測試

但是實際操作中,會遇到的問題還是挺多的:

  1. 設計產品功能的時候,就能定義好通用語言
  2. 驗收標準具備可測量性
  3. 業務用例考慮能否充分。從何而來,是否有足夠的場景支撐
  4. 業務程式碼本身是否具備可測試性
  5. 基礎設施是否能長期支撐 CI/CD
  6. 團隊是否能接受這種開發的模式

… …

當然我們不能指望一個工具可以解決需求模糊的問題,Cucumber只是去解決這個問題中的一環,團隊更加需要的是一套流程、一系列實踐和改變的決心。它需要團隊成員的通力合作,才可以幫助整個團隊更好的理解業務,理解軟體,理解這個複雜的客觀世界。

文章對應的程式碼已經放在了GitHub上://github.com/whthomas/cucumber-demo


附:如何安裝Cucumber

如果要使用Cucumber在項目中可以添加如下的依賴:

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

如果是使用JUnit5,可以把第二個依賴包改成是:

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