阅读视图

发现新文章,点击刷新页面。
🔲 ☆

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

March 24 21:45~23:14

▲圖1:電梯的規格範例

 

前言

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

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

三月初的時候實驗室一組研究IoT的學生跟Teddy介紹他們用python開發的工具—concurrentSpec,它擴充Gherkin語意讓開發人員撰寫並執行同步行為規格。原本Teddy開發ezSpec並沒有計畫要支援描述平行處理行為,聽完學生的介紹,覺得加上這個功能可以讓描述行為的語意變得更完整,因此花了點時間在ezSpec中支援這個功能。

***

平行運作行為的範例

請參考圖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》,圖1的中文敘述是請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/Then依然會繼續執行(因為指定ContinuousAfterFailure參數)。請注意,圖2中的Step執行順序如下,Given, When, Then, Then是循序執行,第一個Then與後面兩個And為平行執行。

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

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


▲圖2:用ezSpec描述電梯規格範例

 

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


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

***

電梯門打不開

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





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

 

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


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

***

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

 

▲圖6:加上ContinuousAfterFailure參數,即使電梯未停妥也繼續執行後續驗證步驟

 

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


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

***

 

結論

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

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

***

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

🔲 ⭐

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

March 21 18:06~19:30

▲一個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 陣列,這就是存放參數的地方。

 

▲圖1:JUnit 5的ArgumentsProvider 介面

 

▲圖2:JUnit 5的Arguments介面

***

ezSpec的Junit5Examples

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

 

▲圖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。

 

▲圖4:Rename a workflow 範例

 

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

 

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

 


▲圖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來獲得測試資料。

 

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

 

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


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

***

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

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

 

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

 

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


▲圖10:MoveLane Scenario Outline範例

 

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


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

***

結語

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

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

***

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

🔲 ☆

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

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



前言

介紹完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的時候如何指定參數、讀取參數,以及傳遞參數的方式。

 

▲圖1:ezSpec的Scenario範例

***

指定與讀取無名參數

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

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

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

 

▲圖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來讀取參數內容。

 

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

***

讀取其他Step的參數

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

 

▲圖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行)。

 

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

***

結語

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

***

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

❌