---
# System prepended metadata

title: BDD SpecFlow使用
tags: [UnitTest, Net, TDD]

---

###### tags: `Net` `UnitTest`

# SpecFlow使用

For近期須分享TDD，想到之前上BDD課程筆記，課程只教BDD概念，雖然有實作，但C#實作上沒使用到SpecFlow去編輯DSL（Domain-Specific Language，特定領域語言）語法去對應產出程式碼，所以自行嘗試了一下。網路蠻多用法文章，但都是最簡單的Context，所以剛好課程有提供稍微接近實作一點的例子來嘗試看看。過程中有遇到一些問題，所以稍微紀錄一下。

## BDD簡易概念

### 簡易概念
BDD 則是將開發過程聚焦在軟體的行為和需求上。強調用自然語言(接近人看得懂的語言)描述軟體應該具有的行為，從而讓開發人員、測試人員和非技術人員（如產品經理、業務分析師等）都能清楚地理解需求。

### 與TDD差異

舉個例子，如果一個計算機程式要做加法功能

 - TDD
     - 撰寫一個測試Case，檢查模擬加法功能（例如，測試 2 + 3 = 5）。
     - 實際撰寫加法功能
     - 測試
     - 沒通過測試
     - 重構
     - 測試.重構.直到測試通過為止

 - BDD
     - 用自然語言描述加法功能的期望行為，例如：當用戶輸入兩個數字並按下「相加」按鈕時，應用程式應該顯示這兩個數字的和。
     - 撰寫檢查加法的功能測試，通常使用一個 BDD 測試框架（如 Cucumber、SpecFlow 等）以自然語言描述行為。
     - 實際撰寫加法功能
     - 執行測試，確保測試通過
     - 重構在測試
    
TDD 的核心理念是先編寫測試，再實現功能。開發人員首先為一個功能或模組撰寫測試用例，然後編輯相應的程式碼，以使這些測試用例通過。關注的是單個函數或方法的測試，而 BDD 則關注的是整個功能或系統的行為。在 BDD 中，測試用例更接近用戶需求，因此有助於提高開發團隊和非技術人員之間的溝通和理解。

## 練習使用SpecFlow ([Test Context請點我](https://github.com/spyua/BDD-Test-For-Net.git))

SpecFlow 是一個用於 .NET 平台的 BDD 工具，可以透過他編寫DSL語言並產出測試Code框架。

### Step1:安裝
 -  VS IDE 安裝 SpecFlow (至延伸模組安裝，安裝完後記得重啟IDE)
 -  新增Nunit專案
 -  測試專案安裝SpecFlow.NUnit (從Nuget安裝，注意Nun)

### Step2:測試專案新增feature檔案，並編輯User Story與Scenario

新增feature檔案，並在Feature編輯User Story

```
Feature: Add item to cart
    In order to avoid the wrong total price
    As a Customer
    I want to get the current total price of items in the cart
```

>  User Story（用戶故事）是一種描述一個應用程式的使用者需求的情境描述。由三個元素構成：角色（Role）、功能（Feature）和優先順序（Priority）。例如「作為一個用戶，我希望能夠在購物車中加入商品」；優先順序是指每個功能的重要性，通常使用一個數字表示，例如 1 表示最高優先順序。這種由角色、功能和優先順序構成的 User Story 模板通常為「As a (role), I want (feature), so that (benefit)」這樣描述組成，可以幫助團隊更好地定義和理解系統需求。

 接著編輯Scenario
 
```
Scenario: Calculate the total price for cart items
    Given there are cart items
    When the customer completes the order
    Then the total price should be the sum of the subtotal of all the cart items plus shipping fee NTD 60

Scenario: Alert customer when adding too many items
    Given there are five items in the cart
    When the customer tries to add one more item
    Then the system should alert the customer not to do so and indicate which item cannot be added

Scenario: Alert customer when exceeding purchase limit
    Given the customer has added a quantity of items to the cart
    When the quantity of items exceeds the purchase limit
    Then the system should alert the customer and indicate that the item has reached its purchase limit

Scenario: Apply free shipping for orders over 500
    Given the customer selects the cart items
    When the total price is over 500
    Then the customer should receive free shipping
```


Scenario是一個用來描述特定功能行為的描述性語句。它通常使用“Given-When-Then”（假定-當-那麼）語法來說明軟體應該如何在特定情況下表現。Scenario可以作為一個可讀且易於理解的需求說明，幫助開發人員和非技術團隊成員達成共識。假設我們有一個購物網站，我們可以為“將產品添加到購物車”功能撰寫以下Scenario：

```
Scenario: 用戶將產品添加到購物車
  Given 用戶已登錄購物網站
  And 用戶瀏覽一個產品頁面
  When 用戶點擊“添加到購物車”按鈕
  Then 產品應該被添加到購物車中
  And 購物車內的產品數量應該增加
```

### Step3:Generator測試Code框架

直接在feature檔案上按右鍵，點選Define Step，就會自動幫你產生測試Code框架，且名稱都是Follow BDD Scenario的描述

![](https://i.imgur.com/9u0dPGo.png)


### Step4:範例寫好後直接按測試即可

![](https://i.imgur.com/0SUqoyn.png)


## 需注意的點

### 產生程式碼結果異常

在Define產生程式碼時，在Multi Scenario下很容易生不出Code來，最常見的就是露掉一些情境的程式碼沒產生，這時候沒產生成功的描述會反紫色如下

![](https://i.imgur.com/hHOBMTx.png)

處理方法有
 
 - 1. 重新撰寫描述，這邊建議可以使用ChatGPT4.0協助生成...不然英文不好以及使用不熟，會卡在不知道如何描述情境讓SpecFlow可以識別.
 - 2. 對反紫色的句子按右鑑選擇Define，有個Copy選項手動貼上(這個處理方法最快
 
### Multi Scenario物件初始化使用ShareContext寫法
 
 如果一個Feautre有多個Scenario，物件要做初始設定可以使用ShareContext寫法，根據提供的情境範例如下
 
```csharp=
// ShareContext宣告要使用的物件
public class SharedContext
{
    public Action addToCart;
    public Cart Cart { get; set; }
    public CardItem Erasier { get; set; }
    public CardItem Pencial { get; set; }
    public CardItem BluePen { get; set; }
    public CardItem Ruler { get; set; }
    public CardItem Notebook { get; set; }
    public CardItem PencilSharpener { get; set; }
}
```

```csharp=
// 在測試物件做下述注入設定
private readonly SharedContext _sharedContext;

public AddItemToCartStepDefinitions(SharedContext sharedContext)
{
    _sharedContext = sharedContext;
}

[BeforeScenario]
public void Setup()
{
    _sharedContext.Cart = new Cart();
    _sharedContext.Erasier = new CardItem(name: "Erasiers", unitPrice: 10, maxPurchaseQty: 10);
    _sharedContext.Pencial = new CardItem(name: "Pencial", unitPrice: 20, maxPurchaseQty: 10);
    _sharedContext.BluePen = new CardItem(name: "BluePen", unitPrice: 30, maxPurchaseQty: 10);
    _sharedContext.Ruler = new CardItem(name: "Ruler", unitPrice: 30, maxPurchaseQty: 10);
    _sharedContext.Notebook = new CardItem(name: "Notebook", unitPrice: 50, maxPurchaseQty: 5);
    _sharedContext.PencilSharpener = new CardItem(name: "PencilSharpener", unitPrice: 50, maxPurchaseQty: 5);
}
```

接著在測試Code直接使用_sharedContext物件即可

