l

2023年3月25日 星期六

使用ezSpec落實行為驅動開發與實例化需求(8):規範平行行為

March 24 21:45~23:14

截圖 2023-03-24 下午10.15.30

▲圖1:電梯的規格範例

 

前言

ezSpec與Gherkin相關的基本功能已經介紹完畢,今天介紹一個Gherkin沒有的功能:「描述平行行為的規格」。

Gherkin的Given, When, Then, And, But這些Step依據它們出現在Scenario的先後順序依序執行,對於一些天生就具有平行處理能力的系統,例如在IoT系統中,多個sensor或device彼此之間都是獨立且平行執行。在這種情況下,用Gherkin就無法表達這些平行執行的行為。

三月初的時候實驗室一組研究IoT的學生跟我介紹他們開發用python開發的工具—concurrentSpec,它可以擴充Gherkin語意讓開發人員撰寫並執行同步行為規格。原本Teddy開發ezSpec並沒有計畫要支援描述平行處理行為,聽完學生的介紹,花了點時間把ezSpec改成類似concurrentSpec的功能,可以幫同步行為撰寫規格。

***

平行運作行為的範例

請參考圖1,該例子節錄自 <Specifying Internet of Things Behaviors in Behavior-Driven Development: Concurrency Enhancement and Tool Support> 這篇論文,論文的作者就是實驗室IoT小組的成員以及Teddy的指導教授鄭老師。論文中提到一個測試電梯的例子,這個例子參考自 Jackson所寫的兩本書:《Software Requirements & Specifications: A Lexicon of Practice, Principles and Prejudices》與《Problem Frames: Analysing and Structuring Software Development Problems》,中文敘述是請ChatGPT幫忙翻譯。

這個例子如果用標準的Gherkin執行,假設第29行執行失敗(無法打開緊急指示燈),測試案例就會停在第29,後面的assertion就不會被驗證。在電梯的例子中,這種行為很顯然是不正確的。因為就算29行與30行都失敗,只要第28行成功(電梯有停在最近的樓層),第39行就應該被檢查(電梯門要在五秒內打開)。

有些測試框架可以讓使用者設定:「就算某一個assertion失敗,測試案例還是持續執行」。但就算是採用這種方式來執行圖1的例子,最後結果還是可能錯誤。例如第28行失敗但第31行成功,這表示:「電梯沒有成功停在最近的樓層,但是電梯門最後卻打開了。」這顯然不是使用者所期待的行為。

怎麼用Gherkin描述同步行為?concurrentSpec提出一個很簡單的擴充方式:「在不增加Gherkin keyword的前提之下,將Given, When, Then當作同步執行群組的起頭,它們之後的And與But將會與它們同步執行。整個同步群族執行完畢之後,才會開始執行下一個同步群組。」換句話說,Given, When, Then彼此之間是循序執行,但它們之後若接著And或But,這些And/But將與它們平行執行。

此外,還可以指定每一個Step如果執行失敗,之後的Step是否要繼續執行。

***

用ezSpec描述圖1行為的程式如圖2所示,Teddy故意讓「打開緊急指示燈」與「取消該叫車請求」發生錯誤(839行與842行),但是這兩個Step如果發生錯誤,下一個Given/When/Thne依然會繼續執行(因為指定ContinuousAfterFailure參數)。請注意,圖2中的Step執行順序如下,Given, When, Then, Then是循序執行,第一個Then與後面兩個And為平行執行。

  • Given
  • When
  • Then, And, And (834、838、841行):三個Step同步執行
  • Then

圖2中有一個小細節要注意,就是要讓Scenario以平行的方式執行,必須呼要ExecuteConcurrently()

截圖 2023-03-24 下午10.37.12

▲圖2:用ezSpec苗電梯範例的規格

 

圖3顯示圖2執行結果,可以看到兩個And執行失敗並沒有影響後續Then的執行。

截圖 2023-03-24 下午10.43.36

▲圖3:圖2執行結果報表

***

電梯門打不開

Teddy修改一下電梯的Scenario,故意讓電梯沒有停在最近樓層,請參考圖4。

截圖 2023-03-24 下午10.57.11

▲圖4:模擬電梯沒有停在最近的樓層。

 

修改後的Scenario執行結果如圖5,可以看到因為Then沒有加上ContinuousAfterFailure參數,所以整個同步執行群組織行完畢之後,就不會繼續執行下一個Step。

截圖 2023-03-24 下午10.56.31

▲圖5:圖4執行結果報表

***

看到這裡鄉民們可能會想:「不管電梯有沒有停在最近樓層,我就是要執行最後一個步驟的檢查啊!」也可以,修改一下Then,幫它加上ContinuousAfterFailure,請參考圖6。

 

截圖 2023-03-24 下午10.57.11

▲圖6:即使電梯未停妥也繼續執行後續驗證步驟

 

修改後的Scenario執行結果如圖7,如果系統行為真的如此,恭喜你,找到一個bug。什麼bug?電梯未停靠最近樓層,但電梯門卻打開了。

截圖 2023-03-24 下午11.04.12

▲圖7:圖6執行結果報表

***

 

結論

如果鄉民們的系統與IoT系統類似,有著很明顯的平行行為,那麼標準的Gherkin與法就無法描述這些行為。借用concurrentSpec所擴充的Gherkin語意,可以在不增加Gherkin keyword的前提之下,使用ezSpec描述平行系統的行為。

ezSpec指定行為的功能差不多介紹完畢,下一集介紹ezSpec產生報表的功能。

***

友藏內心獨白:快到山頂了。

2023年3月24日 星期五

使用ezSpec落實行為驅動開發與實例化需求(7):將共用步驟寫在Background

March 23 21:08~22:20

截圖 2023-03-23 下午10.19.07

▲第七集了,快到山頂了沒XD

 

前言

前幾集介紹如何撰寫Scenario與Scenario Outline,基本上已經可以處理絕大部分的問題。今天介紹Background,讓鄉民們可以共用不同Scenario之間相同的步驟。

***

Background也是一種Scenario

ezSpec將Background也視為一種Scenario,但它身上通常只有Given與And,用來準備不同Scenario中共用狀態的內容。請參考圖1,首先使用第42行newBackground()開啟一個新的Background。與Scenario和Scenario Outline相同,Background一定要隸屬於某一個Story。

新增完Background之後,就可以和寫Scenario一樣,撰寫Step。圖1第44行將backgroundSideEffect這個data member的值設為100,然後在47行將UserId設為UUID。

截圖 2023-03-23 下午9.17.17

▲圖1:ezSpec的Background範例

***

 

圖2的Scenario因為可以直接使用到Background的Given與And,所以它就不需要再撰寫一次Given與And,所以它的第一個Step直接就是第61行的When。在第63行中檢查backgroundSideEffect的值使否為100,如果是,就表示Background有先輩執行,接著再執行這個Scenario。第66行則是讀取Background所新增的UserId。

截圖 2023-03-23 下午9.47.18

▲圖2:在ezSpec的Scenario中讀取Background所設定的資料範例

 

圖3為執行圖2所產生的報表,第9到13為Background的內容,第15~18行為圖2的內容。由這個報表可以很清楚的看出來,雖然Background可以達到重用Step的優點,但使用它的Scenario閱讀起來會變得有點……斷頭…..的感覺。Given不見了,直接從When開始。也就是說,使用者要從重用性與可讀性之間做出取捨。當然也可以修改報表產生程式,重複將Background的Step複製到每一個使用它的Scenario,以提升一點可讀性。

截圖 2023-03-23 下午9.55.14

▲圖3:執行使用Background的Scenario所產生的報表

***

 

結論

寫到這裡,ezSpec的基本功能算是介紹完畢。在Gherkin 6中新增一個Rule關鍵字,用來將Scenario分類到某一個業務規則。Teddy覺得這個關鍵字可以用JUnit 5內建的@Tag annotation達到類似效果,所以暫時沒有在ezSpec中直接支援。

ezSpec除了基本的Gherkin語法以外,也參考了concurrentSpec,支援描述併行行為的功能,下一集介紹這個功能。

***

友藏內心獨白:共用經常會降低可讀性。

2023年3月23日 星期四

使用ezSpec落實行為驅動開發與實例化需求(6):Scenario Outline

March 21 18:06~19:30

截圖 2023-03-21 下午7.48.52

▲一個Test Method,N個綠燈

 

前言

上一集介紹在ezSpec中四種撰寫Scenario Outline的方式(請參考<使用ezSpec落實行為驅動開發與實例化需求(5):撰寫Scenario Outline的四種方法>),所使用的Examples是比較簡單的單一表格。這一集介紹當Scenario Outline的Examples比較複雜的時候,該如何表達這些Examples。

***

JUnit 5的ArgumentsProvider

在說明如何表達複雜的Examples之前,先介紹兩個基本的介面/類別:ArgumentsProvider與Junit5Examples。

JUnit 5 的@ParameterizedTest原本就支援好幾種提供測試資料的方式,上一集使用最簡單的@CsvSource,這一集要用ArgumentsProvider來提供測試資料。參考圖1,ArgumentsProvider是一個介面,只有一個provideArguments method,回傳Stream<? extends Arguments>。

Arguments也是一個介面,參考圖2,它身上有一個get mehtod,回傳 Object 陣列,這就是存放參數的地方。

 

截圖 2023-03-21 下午6.54.02

▲圖1:JUnit 5的ArgumentsProvider 介面

 

截圖 2023-03-21 下午6.55.48

▲圖2:JUnit 5的Arguments介面

***

ezSpec的Junit5Examples

ArgumentsProvider是JUnit 5 設計給的@ParameterizedTest使用的介面,ezSpec的Scenario Outline接受的參數是Examples,兩者介面不同。因此Teddy設計一個Junit5Examples抽象類別,透過它可以轉換ezSpec的Examples以及 JUnit 5的ArgumentsProvider,如圖3所示。

 

截圖 2023-03-21 下午7.02.37

▲圖3:ezSpec的Junit5Examples抽象類別

***

 

RenameWorkflow Use Case的Scenario Outline

參考圖4,假設你要幫ezKanban的RenameWorkflow使用案例定規格,你想到兩個主要的Scenarios:

  • Scenario 1:新的名稱與舊的名稱不同,執行RenameWorkflow使用案例之後會產生一個領域事件。
  • Scenario 2:新的名稱與舊的名稱相同,執行RenameWorkflow使用案例之後不會產生領域事件。

針對這兩個Scenario,你一共找出五種不同的測試資料,例如把Workflow的名字從dev改成deV、從dev改成DEV,或是從dev改成dev。

 

截圖 2023-03-21 下午7.06.18

▲圖4:Rename a workflow 範例

 

圖4的Scenario Outline,要如何用ezSpec來表達?首先定義測試資料,請參考圖5與圖6。

 

截圖 2023-03-21 下午7.13.52

▲圖5:不同Workflow名稱的測試資料(Examples)

 

截圖 2023-03-21 下午7.14.03

▲圖6:相同Workflow名稱的測試資料(Examples)

 

準備好Examples之後,就可以在Scenario Outline中使用它們。參考圖7,採用上一集所介紹的方法一來撰寫Scenario Outline,唯一不同處在第61行:

WithExamples( Junit5Examples.get(different_name_examples.class),

                          Junit5Examples.get(same_name_examples.class))

直接指定Examples的class name來獲得測試資料。

 

截圖 2023-03-21 下午7.17.22

▲圖7:使用Junit5Examples的具體類別當作測試資料來源提供給ezSpec

 

另外,different_name_examples與same_name_examples也可以作為@ParameterizedTest的資料來源,用來執行Scenario Outline的,請參考圖8。

截圖 2023-03-21 下午7.22.25

▲圖8:使用Junit5Examples的具體類別當作測試資料來源提供給@ParameterizedTest

***

更複雜的測試資料:表格中還有表格

請參考圖9,這是提供給ezKanban的MoveLane使用案例的驗收測試資料,其中givenWorkflow表示在Given階段初始化的Workflow狀態,expectedSubStageAsRootStage表示移動Lane之後Workflow的狀態。

 

截圖 2023-03-21 下午7.28.24

▲圖9:Examples包含表格的範例

 

MoveLane的Scenario Outline如圖10所示,可以看到在89行指定了<given_workflow>參數(圖9中的givenWorkflow表格),以及在123行指定了<expected_workflow>參數(圖9中的expectedSubStageAsRootStage表格)。

截圖 2023-03-21 下午7.33.36

▲圖10:MoveLane Scenario Outline範例

 

圖10的執行結果如圖11所示,可以看到第22是最外層的Examples表格,其內部的given_workflow與expected_workflow這兩個column各是另一個表格。可以透過ezSpec很簡單的設計複雜的驗收測試資料,使用上也很簡單。

截圖 2023-03-21 下午7.38.56

▲圖11:MoveLane Scenario Outline執行結果報表(只顯示部分)

***

結語

透過ezSpec的Junit5Examples可以設計同時讓ezSpec與JUnit 5的ParameterizedTest所使用的驗收測試資料,每一種測試資料獨立定義在一個Junit5Examples類別中,容易管理與撰寫。

寫到這裡,常用的ezSpec功能已經介紹完畢。下一集介紹Background,可以用來將相同的Given寫在一起,以利重複使用與減少重複程式碼。

***

友藏內心獨白:Table內的Table,還是Table。

2023年3月22日 星期三

使用ezSpec落實行為驅動開發與實例化需求(5):撰寫Scenario Outline的四種方法

March 21 09:47~12:28

截圖 2023-03-21 上午10.43.41

▲圖1:Scenario Outline範例

 

前言

前兩集介紹在ezSpec中撰寫Scenario的方式(請參考<使用ezSpec落實行為驅動開發與實例化需求(3):撰寫Scenario與傳遞簡單參數> 與 <使用ezSpec落實行為驅動開發與實例化需求(4):在Scenario中使用表格>)。當你開始採用BDD(行為驅動開發)或SBE(實例化需求)開發系統,針對同一個Feature或Story中的多個Scenario,很可能發生執行步驟相同或相似,只有輸入的資料以及在When中所用來檢查的expected result不同的情況。此時可考慮將這些Scenario改寫成Scenario Outline,用以簡化feature file的撰寫。

ezSpec支援四種不同的方式撰寫與執行Scenario Outline,以下逐一介紹。

***

方法一:用For Each執行Scenario Outline

Scenario翻譯成劇情場景,Scenario Outline則是劇情大綱場景大綱。簡單來說,Scenario Outline就是餵一組資料給Scenario,讓這個Scenario執行好幾次。用傳統測試的術語,這種測試稱為參數化測試案例資料驅動測試案例

圖1是一個簡單的Scenario Outline,其目的為依據商品未稅金額計算含稅總價,一共有四個例子(四筆測試資料),分別涵蓋四捨五入、可開立零元發票以及兩個邊界條件。為了撰寫這四個例子,最無腦的方式是把它們寫成四個Scenario。但是這樣一來這四個Scenario除了測試資料不同以外,其他的執行步驟都一樣,也就是說寫成四個Scenario會產生很多重複程式碼,此時改用Scenario Outline就可以解決這個問題。

為了儘量與JUnit 5整合,ezSpec提供好幾種撰寫Scenario Outline的方式。首先看到圖2:

  • 第74行:執行 newScenarioOutline() 在Story身上產生一個新的Scenario Outline。
  • 第75行:用 WithExamples(examples) 將指定該Scenario Outline所使用的Examples(也就是測試資料)。
  • 第76行:呼叫 RuntimeScenarios() 獲得List<RunimeScenario>。Scenario Outline會依據所輸入的Examples資料筆數,產生N個RunimeScenario。
  • 第77行:使用 forEach(example -> {} ) 的方式來指定每一次執行RunimeScenario的Step。

之後的寫法就跟一般的Scenario一樣,唯一的差別在於讀取Examples參數的方式。在標準的Scenario中,透過env.row() 可以讀取表格中的資料。在Scenario Outline的情況下,RunimeScenario每次只會看到一筆Examples的資料,此時要用env.getInput().get(column_name) 的方式來讀取Examples的資料,如圖2的82~85行所示。

 

截圖 2023-03-21 上午10.55.59

▲圖2:ezSpec的Scenario Outline範例,採用forEach執行

 

由於圖2的Scenario Outline只是一般標準的test method,因此在JUnit的報表中,只會看到一筆執行結果,如圖3。

 

截圖 2023-03-21 上午11.08.48

▲圖3:圖2執行結果的JUnit報表

 

雖然JUnit報表中只會有一筆資料,但ezSpec產生的報表會包含Scenario Outline每次的執行結果,請參考圖4。

截圖 2023-03-21 上午11.10.25

▲圖4:圖2執行結果的ezSpect報表

***

方法二:用@ParameterizedTest執行Scenario Outline

如果開發人員希望在JUnit的報表中看到Scenario Outline每一次執行的結果,可以改用JUnit 5的@ParameterizedTest來執行Scenario Outline,請參考圖5。JUnit 5的ParameterizedTest直接將每次執行的測試資料透過test method的arguments接收資料,ParameterizedTest支援多種指定測試資料(相當於圖1的Examples)的方法,圖5所示為透過@CsvSource的方式指定測試資料。

使用ezSpec透過ParameterizedTest撰寫Scenario Outline的方式與圖2類似,除了將原本的@Test換成@ParameterizedTest,以及使用@ParameterizedTest所規定的設定測試資料方式以外,主要有以下兩點不同之處:

  • 不需要自行使用for each:因為JUnit 5的ParameterizedTest會自動執行該test method好幾次,因此開發人員就不需要如同圖2中透過RuntimeScenarios().forEach的方式來執行RuntimeScenarios。
  • 用WithParameterizedExamples取代圖2的WithExamples:由於ParameterizedTest的測試資料不包含表格的表頭,因此需要透過WithParameterizedExamples將測試資料的表頭告知ezSpec,如此一來在描述每一個Step的時候,若使用到 <tax_excluded> 等變數時,ezSpec才知道要如何將這些變數替換成測試資料中實際的數值。

 

截圖 2023-03-21 上午11.33.23

▲圖5:使用@ParameterizedTest執行ezSpec的Scenario Outline

 

使用ParameterizedTest有一個好處,就是JUnit的報表可以顯示每次Scenario Outline的執行結果,如圖6所示。

截圖 2023-03-21 上午11.29.41

▲圖6:圖5執行結果的JUnit報表

 

***

方法三:用@EzScenarioOutline執行Scenario Outline

開發人員也可以使用ezSpec的@EzScenarioOutline來執行Scenario Outline,如圖7所示。在這種情況下就不需要使用RuntimeScenarios().forEach的方式來執行RuntimeScenarios,ScenarioOutline的內容除了要呼叫WithExamples以外,基本上與Scenario沒什麼差別。主要差異有以下三點:

  • @EzScenarioOutline要自行執行執行次數,以圖7的例子,因為測試資料有四筆,因此指定@EzScenarioOutline(4)
  • test method需要接受一個型別為RepetitionInfo的參數,如圖7第119行。
  • 要改用Execute(repetitionInfo.getCurrentRepetition())來執行RuntimeScenarios,如圖7第138行。

 

截圖 2023-03-21 上午11.55.04

▲圖7:使用@ParameterizedTest執行ezSpec的Scenario Outline

 

 

使用@EzScenarioOutline可以達到和@ParameterizedTest類似的效果,在JUnit的報表中顯示每次Scenario Outline的執行結果(但無法顯示文字說明),如圖8所示。

截圖 2023-03-21 下午12.03.48

▲圖8:圖7執行結果的JUnit報表

***

方法四:用@DynamicScenario執行Scenario Outline

最後一種執行Scenario Outline的方式,使是用ezSpec的@DynamicScenario,請參考圖9。@DynamicScenario的撰寫方式和@EzScenarioOutline大致相同,不同點有:

  • test method須回傳DynamicNode,如圖9第141行。
  • DynamicExecute()取代Execute(),如圖9第160行。
  • return整個DynamicExecute執行結果,如圖9第149行。

 

截圖 2023-03-21 下午12.09.18

▲圖9:使用@DynamicScenario執行ezSpec的Scenario Outline

使用@DynamicScenario在JUnit的報表中可以顯示最詳細的資料,除了每次Scenario Outline的執行結果,還包含每一個Step的內容與執行結果,如圖10所示。

 

截圖 2023-03-21 下午12.17.54

▲圖10:圖9執行結果的JUnit報表

***

結語

本集介紹四種不同撰寫與執行Scenario Outline的方法,除了撰寫方式略有不同,以及JUnit產生的報表有所差異以外,其執行結果ezSpec所產生的報表基本上都是相同的。開發人員可以選擇自己認為比較喜歡的與法表達方式來撰寫Scenario Outline。

在本集中Scenario Outline的Examples是一個簡單的表格,下一集將介紹如何在Scenario Outline中指定表格中含有表格的測試資料。

***

友藏內心獨白:太多選擇也是一種煩惱。

2023年3月21日 星期二

使用ezSpec落實行為驅動開發與實例化需求(4):在Scenario中使用表格

March 21 05:40~6:45

截圖 2023-03-21 上午5.49.00

▲圖1:使用ezSpec在Scenario中讀取單一表格資料範例,表格內容取自《BDD in Action》一書

 

前言

上一集介紹如何使用ezSpec撰寫Scenario、從Step描述文字內容中讀取參數、以及在不同的Step Definition之間傳遞資料的方法(請參考<使用ezSpec落實行為驅動開發與實例化需求(3):撰寫Scenario與傳遞簡單參數>),今天介紹如何在Scenario中讀取表格資料。

***

讀取單一表格

請參考圖1的85~87行,ezSpec支援Gherkin的表格,以 | 符號分隔不同的column。通常表格的第一列為表頭,之後每一列為表格內容。ezSpec支援兩種讀取表格的方式:

  • row(int).get(column_name):第一種讀取表格方式請參考圖1的88~90行,env.row(0)拿到表格中第一個row,也就是 |    Jill    |   100,000     |   800           |   這一組資料。接著可以用get(column_name)拿到這個row中不同column的資料,例如 env.row(0).get("owner")拿到Jill。
  • row(first_column_data_in_each_row).get(column_name):圖1的93~95行展示第二種讀取表格方式,用每一個row第一個column的資料當作key,使用env.row(first_column_data_in_each_row)的方式定位到所要讀取的該筆row,然後再用get(column_name)拿到這個row中不同column的資料。例如env.row("Jow").get("points")拿到50,000。

使用表格可以一次指定多筆測試資料,提高Scenario的可讀性。

***

讀取多個表格

你也可以在一個Scenario中使用多個表格,例如把一個表格當作Given裡面的輸入資料,另一個表格放在Then裡面當作expected result,請參考圖2。

讀取多個表格資料的方式與讀取單一表格相同,但有一點需要注意:「在ezSpec中,一個Scenario同一時間最多只會有一個Active Table。」在圖1中由於該Scenario只定義了一個表格,因此一但表格定義之後,在接下來的Step Definition(Given-When-Then-And)中都可以讀到該表格的資料。在圖2中,第一個表格定義在Given,第二個表格定義在Then。因此Given以及When會讀到第一個表格的資料,而Then則是讀到第二個表格的資料,請參考圖2第171~172行(後面定義的表格會蓋掉上一個定義的表格)。

截圖 2023-03-21 上午6.15.54

▲圖2:使用ezSpec在Scenario中讀取多個表格資料範例,表格內容取自《BDD in Action》一書

***

結語

這兩集介紹在ezSpec中撰寫Scenario的方式,有使用過Cucumber、SpecFlow或JBehave的鄉民,應該可以很明顯發現兩者的不同之處。因為ezSpec是一種Internal DSL,使用「類Gherkin語法」以Java程式語言採用Specification by Example的方式,用例子(Scenario)來表達規格。

ezSpec的Step Definition以Lambda實作,和其相對應的Step直接綁定,不需要因為Step的文字內容改變而重新產生一個新的Step Definition,可以讓開發人員專心在撰寫Scenario上面,少掉很多工具所帶來的干擾。

採用BDD或Specification by Example的開發方式,經常會產生「執行步驟相同但驗證資料不同的Scenario」,此時就可以用下一集介紹Scenario Outline來簡化這些相似的Scenario。

***

友藏內心獨白:一表值千言。

2023年3月20日 星期一

使用ezSpec落實行為驅動開發與實例化需求(3):撰寫Scenario與傳遞簡單參數

March 18 20:27~22:12;March 20 00:00~00:30

截圖 2023-03-20 上午12.20.34

 

前言

介紹完ezSpec的領域模型(請考<使用ezSpec落實行為驅動開發與實例化需求(1):領域模型介紹>)與Feature和Story(請考<使用ezSpec落實行為驅動開發與實例化需求(2):Feature與Story>),終於要進入主題,今天介紹如何用ezSpec撰寫Scenario。

***

撰寫Scenario

假設你要開發一隻開發票的程式,可以從未稅金額計算含稅總價與稅金,你幫這隻程式寫的第一個Scenario如圖1所示:「當一台電腦的未稅金額為20000,營業稅為5%,當你買了這台電腦,你應該支付含稅金額21000的總價,其中1000元為營業稅。」

產生Scenario的方式很簡單,透過feature.withStory()找出事先建好的Story(請考<使用ezSpec落實行為驅動開發與實例化需求(2):Feature與Story>),然後呼叫Story身上的newScenario就可以產生一個新的Scenario。在產生Scenario的時候你可以指定這個Scenario的名字,如果未指定則ezSpec會自動抓取test method的名字當作Scenario的名字。

產生Scenario之後,可以透過它的Given、When、Then、And、But(統稱為Step)等method來撰寫Scenario的實際內容。每一個Step接受兩個參數:

  • 字串:用來描述Step內容的文字敘述。
  • Lambda:實際實行Step的程式,ezSpec使用Lambda來取代Cucumber的Step Definition。

由於本系列文章的主要目的是介紹ezSpec的使用方式,並不是要介紹透過TDD/BDD/SBE來開發軟體,所以接下來Teddy會說明撰寫Scenario的時候如何指定參數、讀取參數,以及傳遞參數的方式。

 

截圖 2023-03-18 下午9.33.45

▲圖1:ezSpec的Scenario範例

***

指定與讀取無名參數

在圖1的範例中,一共有四個參數:

  • 第61行的未稅金額20,000
  • 第64行的營業稅5%
  • 第69行的含稅總價21,000
  • 第72行的營業稅1,000

有兩個方法可以在Step的Lambda中讀取這些參數,第一種方法是在這些參數前面加上$符號,Step的Lambda可以傳入一個ScenarioEnvironment的物件當成參數,可以透過ScenarioEnvironment在Lambda中讀取$開頭的字串,請參考圖2中的env參數。

 

截圖 2023-03-18 下午9.39.50

▲圖2:ezSpec指定與讀取無名參數的Scenario範例

 

圖2示範三種讀取參數的函數:

  • 第62行evn.getArg:回傳String型別的參數
  • 第65行evn.getArgd:回傳double型別的參數
  • 第70行evn.getArgi:回傳int型別的參數

透過$方式所指定的參數,只有value,沒有key,因此在讀取時需透過index方式來讀取資料。在圖2中每一個Step剛好都只有一個參數,因此透過env.getArg(0)就可以拿到這些參數。

***

指定與讀取有名參數

既然有無名參數,就有有名參數,請參考圖3:

  • 指定有名參數:參考圖3的61行可用 ${tax_included=20,000},或是用第64${vat_rate:5%}來指定參數的名稱(可用=或:)。
  • 讀取有名參數:參考圖3第62、65、70、73,讀取方式和圖2類似,但此時使用字串的key來讀取參數內容。

 

截圖 2023-03-18 下午9.51.54

▲圖3:ezSpec指定與讀取有名參數的Scenario範例

***

讀取其他Step的參數

有時候你想要在Lambda中讀取其他Step所定義的有名參數,這時候就要用getHistoricalArg函數來取得,請參考圖4第74行:env.getHistoricalArg("tax_included") 讀到第61行所定義的tax_included參數。

 

截圖 2023-03-19 下午11.59.36

▲圖4:ezSpec讀取其他Step的有名參數範例

***

在不同的Step中傳遞資料

在撰寫Scenario的時候經常需要在不同的Step之間傳遞資料,例如在67行的When當中你用hasBought變數代表成功購買到電腦,你想在Then的Lambda中驗證hasBought的內容。請參考圖5,此時可以使用ScenarioEnvironment的put(key, value)將要傳遞的資料放到ScenarioEnvironment(第69行),然後在另一個Lambda中用get(key, Class<T>)將資料讀出(第73行)。

 

截圖 2023-03-20 上午12.09.10

▲圖5:ezSpec在不同的Step中傳遞資料

***

結語

今天介紹ezSpec撰寫Scenario與指定和讀取參數的方式,Scenario也可以接受一個Table的資料當作參數,Teddy將在下一集介紹這個功能。

***

友藏內心獨白:用Lambda撰寫Step Definition就不用處理煩人的正規表示式了。

2023年3月15日 星期三

使用ezSpec落實行為驅動開發與實例化需求(2):Feature與Story

March 15 08:57~11:00;20:02~08:19

截圖 2023-03-15 下午8.18.39

▲用ezSpec寫ezSpec的使用說明文件

前言

上一集<使用ezSpec落實行為驅動開發與實例化需求(1):領域模型介紹>提到Feature是Gherkin用來描述需求的最大單位,今天介紹在ezSpec中如何撰寫Feature。

在介紹撰寫Feature之前,先說明External DSL(外部領域特定語言)Internal DSL(內部領域特定語言)這兩個名詞。使用Cucumber、SpecFlow或Cucumber-JVM這類工具的鄉民,將Feature寫在feature file裡面,再交由工具產生Step Definition,然後撰寫Step Definition以達到驗收測試自動化的目的。Gherkin是一種用來描述需求或規格的DSL(Domain-Specific Language,領域特定語言),Cucumber系列工具的作法Teddy將其稱為External DSL,因為用來描述需求的語言(也就是Gherkin),和開發用的程式語言(Ruby、C#或Java)並不相同,因此Gherkin成為「外部領域特定語言」。

External DSL的好處與缺點Teddy在<無痛將驗收測試文件寫在測試案例中>與<使用ezSpec落實行為驅動開發與實例化需求(1):領域模型介紹>提過,有興趣的鄉民可參考。ezSpec是一種Internal DSL,使用Java語言模擬Gherkin,換句話說用來描述需求或規格的DSL與開發者使用的程式語言是一致的。

***

撰寫第一個Feature

ezSpec除了採用Internal DSL的方式開發,它也與JUnit 5結合,撰寫Feature檔就和傳統寫一個Test Class是一樣的方式。請參考圖1第8行,FeatureSpec是一個一般的Java Class,名稱可以隨便取,你也可以把它叫做FeatureTest或任何你喜歡的名字。

FeatureSpec實作ezSpec interface,這麼做可以讓ezSpec自動產生報表。如果你只需要在IDE裡面觀看JUnit所產生的報表,也可以不用實作這個介面。

在第9行宣告一個static Feature feature 物件,用來代表一個feature file。請注意,如果你要讓ezSpec自動產生報表,這個static Feature的變數名稱一定要取名為 feature,否則系統無法自動產生報表。接著第11~21行使用JUnit 5標準的@BeforeAll annotation來定義feature的內容。第13行的Feature.New()產生一個新的Feature物件,它接受兩個參數:

  • Name:Feature的名稱,用來代表一個交付給使用者的功能或功能組。
  • Description:Feature的詳細內容描述,除了進一步說明Feature的內容,也可以拿來定義這個Feature會用到的詞彙,這些詞彙可以成為通用語言(ubiquitous language)的一部分。

 

截圖 2023-03-15 上午9.51.05

▲圖1:ezSpec 的Feature使用範例

 

定義好feature之後,可以直接寫一個test method來執行看看,驗證feature的內容,請參考第24~40行,這個test method單純驗證feature的內容是否正確,其產生的報表如圖2。

 

截圖 2023-03-15 上午10.26.41

▲圖2:ezSpec產生的Feature報表範例

 

***

Story

ezSpec 的Feature物件本身只是一個「容器」,實際上的需求內容是寫在Scenario裡面。在Feature與Scenario之間,ezSpec還支援Story。一個Feature需要包含至少一個Story,Scenario則是附屬於某一個Story。

請參考圖3,第16行呼叫feature.newStory新增一個Story,它和Feature一樣有Name和Description欄位,其用途也類似。此外,Story還有一個Index欄位,可以指定一個編號給它,之後可以使用這個編號來讀取Story。

基本上Story就是一個小Feature,如果你覺得不需要Story來管理更小的功能,在撰寫Feature的時候,只需要幫每一個Feature寫一個Story,然後由該Story來產生所有的Scenario。

 

截圖 2023-03-15 下午8.11.42

▲圖3:ezSpec 的Story使用範例

 

截圖 2023-03-15 下午8.05.46

▲圖4:ezSpec 產生的Story報表 (未包含內部的Scenario、Scenario Outline與Background)

 

新增Story之後,可以呼叫feature.withStory拿到屬於該feature的story,然後再透過它的newScenario產生Scenario。下一集將介紹如何使用Scenario與Given-Then-Then撰寫需求範例。

***

友藏內心獨白:運動前先暖身。

2023年3月14日 星期二

使用ezSpec落實行為驅動開發與實例化需求(1):領域模型介紹

March 14 18:10~19:02;20:28~23:23

▲圖1:ezSpec領域模型

 

前言

行為驅動開發(Behavior-Driven Development;BDD)實例化需求(Specification by Example)是兩種主流的測試驅動開發方法。藉由開發人員與領域專家或利害關係人一起探討需求(協同建模),以及代表需求的具體範例,並將其表達在自動化測試案例中,以達到活文件(Living Documentation)的效果。

 

Teddy在開發ezKanban的過程中,在2021年順手作了ezSpec—它是用Java撰寫的Internal DSL,可以簡化使用Cucumber或JBehave等工具的麻煩(請參考<無痛將驗收測試文件寫在測試案例中>)。Teddy當初開發ezSpec是想要做Living Documentation的研究,後來ezKanban的事情太忙,做好ezSpec之後只有在ezKanban中使用它,一直沒有時間進一步把它做到完整的支援Living Documentation。這陣子因故又把ezSpec拿出來「積極開發」,等開發到一段落準備把它開源。在開源之前先寫幾篇文章來介紹ezSpec,日後也可當作ezSpec的使用文件。

***

ezSpec的領域模型與使用範例

圖1是ezSpec的領域模型,基本上ezSpec參考Gherkin語法所設計,差別在於ezSpec可以描述Story以及同步執行步驟。這些細節之後再詳細說明,先介紹領域模型各個物件的意義:

  • Feature:功能,代表可以提供使用者價值的單位。關於Feature的「粒度大小」有不同的用法,有人將「整個功能模組」當成一個Feature,然後再透過Story或Scenario切成比較小的功能。例如,把「結帳」當成一個Feature,裡面包含現金結帳、信用卡結帳、xxx Pay等。也有人把Feature當成一個單獨功能使用,粒度大小相當於傳統的使用案例(Use Case)。
  • Story:故事,就是敏捷社群經常使用的User Story。Gherkin/Cucumber本身並沒有支援Story,但JBehave有。有些人主張Story只是開發過程中用來「切割需求」的一種任務分派的單位,最後交付給使用者的是Feature。既然Gherkin是用來描述需求的一種語法,不需要使用Story去紀錄開發過程的「臨時性產品」。但許多傳統敏捷社群的人可能習慣用Story來做為需求溝通的最小單位,所以認為還是有Story比較好。ezSpec支援Story,一個Feature可以有一到多個Story。
  • Scenario:劇情,基本上一個Scenario可以想像成Specification by Example裡面的Example,透過舉例來溝通需求。一個Feature或Story具體來說到底要做什麼?用舉例的方式來表達,比較具體明白,也可以減少誤會。幫一個Feature或Story列舉出幾個具代表性的Scenario之後,團隊如果覺得已經可以瞭解這個功能的內容,這些例子就成為這個功能的驗收條件。如果可以將這些例子(也就是Scenario)變成程式自動執行,它們就成為自動化驗收測試。圖2為ezSpec的Scenario範例,由於ezSpec是一個Internal DSL(Domain Specific Language),使用ezSpec描述規格時就跟寫程式是類似的,直接把Gherkin的Given-Then-Then內容寫在lambda裡面。
截圖 2023-03-14 下午10.44.37

▲圖2:ezSpec Scenario範例,pending()表示該Step尚未實作

 

圖3為ezSpec執行圖2的Scenario所產生的報表。

截圖 2023-03-14 下午10.50.00

▲圖3:ezSpec產生的Scenario執行結果報表

***

  • Scenario Outline:劇情大綱,幫一個Feature或Story舉了幾個例子之後,如果發現這些例子的執行步驟都相同,只有輸入的資料與執行結果不同,就可以將這些個別的Scenario整理成一個Scenario Outline。Scenario Outline基本上就是傳統軟體測試中的「參數化測試」或是「資料驅動測試」,在Gherkin中,提供不同資料的方式是指定一組Examples請參考圖4的ezSpec ScenarioOutline範例,範例表格資料內容參考自https://reurl.cc/qkrL9y

 

截圖 2023-03-14 下午10.20.49

▲圖4:ezSpec ScenarioOutline範例

 

圖4第160~177行是指定Examples的地方,兩組Examples一共以五筆測試資料。第179~187行新增一個Scenario Outline,整個Scenario Outline寫在JUnit 5 的test method內,執行結果與一個一般的測試案例相同,可以在JUnit報表中看到,如圖5。

▲圖5:Scenario Outline執行結果

 

ezSpec可以將整個Feature的執行結果產生文字報表,圖6為上述Scenario Outline執行結果所產生的報表,可以看到每一輪執行的輸入資料,以及每一個步驟(Step,請參考後面說明)的執行結果。

截圖 2023-03-14 下午10.24.48

▲圖6:ezSpec產生的Scenario Outline執行結果文字報表

 

***

 

  • Runtime Scenario:Scenario在ezSpec是一個抽象類別,在執行期間,每一個Scenario,以及Scenario Outline展開之後每次執行都會產生一個Runtime Scenario物件,用來記錄輸入資料與執行結果。
  • Background背景,請參考圖7,類似JUnit的@BeforeEach,可以將多個Scenario的共同步驟定義在Background中,然後再重複使用。Background雖然達到減少重複描述步驟的好處,但是會讓Scenario變得比較不容易閱讀,使用時要稍作取捨。

截圖 2023-03-14 下午10.39.37

▲圖7:ezSpec Background範例

  • Step:步驟,一個Scenario可以包含多個Step。Given、When、Then、And、But這些都屬於Step的一種類型。
  • Given:用來描述執行功能的前置條件。
  • When:用來描述執行功能,也就是傳統測試所說的「執行待測程式」。
  • Then:用來驗證結果功能執行後的結果。
  • And, But:在Given、When、Then之後可以加上And與But作為接續步驟。
  • Concurrent Group:Gherkin的Step只支援循序執行,也就是你無法用Gherkin描述同步行為的規格。Teddy的指導教授鄭老師指導一組研究IoT的學生,用Python開發了一個稱為concurrentSpec的工具,可以使用原本Gherkin的語法來描述IoT系統地同步行為規格。原本ezSpec也只支援循序的Step,最近一個月才參考concurrentSpec,加上描述同步行為的能力。

***

 

結語

傳統使用Cucumber、SpecFlow或是JBehave,都是先用Gherkin撰寫純文字的feature file(或是 story file),再透過工具產生Step Definition Method/Function,然後開發人員負責實作這些Step Definition Method,把規格變成可執行文件。

這種方式,原本是希望領域專家或利害關係人可以直接用Gherkin採用舉例的方式描述需求,再交由開發人員將其轉成自動化驗收測試。概念很好,但這種方式有兩個很大的問題,首先領域專家或利害關係人大該都不會用Gherkin直接幫你寫規格,他們願意口頭跟你溝通、討論,而且是持續地溝通、討論,就已經很了不起了。其次,Step Definition Method很難寫,也不好閱讀與維護。因為要用到很多正規表示式將feature file所描述的內容傳給程式,要先學會不同工具對於Step Definition的繁瑣撰寫規範,實際使用時很容易出錯,讓開發人員無法專心在描述規格與範例上面。既然到頭來feature file與Step Definition都要開發人員自己寫,為什麼不用「開發人員友善」的方法與工具,來做BDD/SBE呢?

改用Internal DSL的方式,直接拿掉煩人的Step Definition與正規表示式,整個BDD/SBE/TDD流程變得很順暢。用Internal DSL的方式並不是Teddy發明的,有人用Ruby做過,只是Teddy一直到2021年在《Living Documentation: Continuous Knowledge Sharing by Design》書中看到一個類似的例子之後,才引發自己也用Java開發類似工具的念頭。

Teddy自己使用的結果表明,真的是方便很多。這一集先到這邊,之後再詳細逐一介紹ezSpec的每一項功能。

***

友藏內心獨白:軟體還沒Open,文件先Open。