l
顯示具有 TDD/BDD 標籤的文章。 顯示所有文章
顯示具有 TDD/BDD 標籤的文章。 顯示所有文章

2023年6月20日 星期二

你就是寫太多測試才會沒時間(2):自動化測試是金字塔嗎?

June 20 09:55~11:46

  


▲圖1:傳統的自動化測試形成金字塔形狀,單元測試占比最大


前言

傳統軟體自動化測試形成如圖1的金字塔形狀,作為驗證個別軟體元件正確性的單元測試數量做多,整合測試次之,透過使用者介面驗證系統功能的使用者驗收測試或稱為End-To-End測試數量最少。

多年來,很多撰寫自動化測試的開發人員心中大致依循著測試金字塔去規劃與撰寫他們的測試案例。但是,雖著單元測試越來越多,系統功能不斷地演化以及持續重構改善設計,開發人員經常會發現:「靠,剛剛的修改造成N個單元測試失敗。更慘的是,我看不懂這些失敗的單元測試為什麼失敗。」

整合測試因為以黑箱的角度測試「系統功能」,因此相對而言對於軟體修改與重構的抵抗力比較高。但光使用整合測試驗證系統品質存在兩個問題:

  1. 發生錯誤時不易除錯(不容易明確看出錯誤發生在哪個地方)
  2. 執行速度比單元測試要慢很多,導致回饋路徑較長,降低開發人員持續測試的意願

如果可以減少單元測試的數量同時維持單元測試的效果(避免上述兩個問題),自動測試有沒有可能從金字塔變成如圖2所示的菱形


▲圖2:自動化測試有沒有可能是菱形?

 

今天這一集先談第一個問題,下一集再談測試執行速度的問題。

***

用合約取代單元測試

測試只是一種驗證系統行為的方法,在軟體工程中除了測試以外還有一種也算是廣為人知但較少人做的方法:合約式設計(Design by Contract;DBC)也可以規範系統行為。DBC的作法很簡單,模仿真實世界人類的合約,幫軟體元件撰寫合約。軟體合約主要包含:pre-conditions(前置條件)post-conditions(後置條件)以及class invariants(類別不變量)。為了簡化起見之後的範例只討論前兩者,先忽略class invariants。

圖3是ezKanban系統中的Workflow aggregate單元測試,這也是傳統用來驗證程式行為的做法。


▲圖3:Workflow單元測試

 

圖4是幫Workflow的建構函數撰寫合約的程式範例,其中第42~44是pre-conditions,第48~61是post-conditions,第46行是method body。當程式執行的時候,只要pre-conditions和post-conditions都通過,那麼不管method body如何實作,它的行為就被視為具備正確性(correctness)


   ▲圖4:幫Workflow寫合約 

***

看到圖4的範例,鄉民們可能會覺得:「類別的合約就是幫method做輸入參數檢查,然後把平時寫在單元測試裡面的assertions移到production code裡面而已啊。」這樣做雖然不用寫單元測試,但是這些合約也是程式碼,也是要花時間撰寫與維護,這樣有省到時間嗎?

這個問題可以從幾個方面來討論:

  • 不用寫arrange和act:撰寫單元測試有三個步驟,arrangeactassert。寫成合約之後,assert部分還是存在,但少了arrange與act。寫過自動化測試的鄉民們應該很有感,很多時候花在arrange的時間甚至比assert還多。減少arrange與act除了少掉撰寫的時間,也避免了之後需求變更或軟體重構導致需要維護單元測試的時間。
  • 和Production Code生活在一起有助於開發與維護:寫在production code裡面的合約(post-conditions),看起來跟寫在測試裡面的assertions很像,但合約並不是把測試寫在production code,而是把規格寫在production code,這兩者有很大的差別。將規格寫在production code,當規格改變之後,可以直接修改production code(反之亦然),減少context switching。
  • Caller和Callee責任清楚:撰寫合約也可以釐清物件之間的責任,Caller需要滿足pre-conditions,Callee需要滿足post-conditions。當合約被違反時,丟出的例外訊息可以協助找出錯誤,這一點可以達到和單元測試一樣,甚至更好的除錯效果。

***

誰來驗證合約?

看到這裡鄉民們應該有一個疑問:「我可以直接執行單元測試,寫在production code裡面的合約要怎麼執行?」另外,「合約也是程式碼,寫錯了怎麼辦?要不要寫測試來驗證合約?」

合約是在系統執行期間(runtime)被執行與驗證,所以還是要有「人」來執行這些合約。這就是圖2中的整合測試所要負擔的責任:透過整合測試來執行合約

有沒有需要另外寫其他類型的測試來驗證合約?基本上不需要,當執行驗收測試時如果合約失敗,和單元測試執行失敗一樣,可能有兩個原因:production code寫錯或測試寫錯(合約寫錯),然後開發人員就必須介入排除錯誤發生的原因。

***

結論

透過驗收測試驅動合約,可以極大幅度減少單元測試的數量,接下來只要可以加速驗收測試執行速度,實務上就有可能落實Teddy所介紹的這套方法。至於如何加速驗收測試執行,這個問題比較複雜,下集再談。

***

友藏內心獨白:「你就是寫太多測試才會沒時間」都是真的 XD。

2023年6月17日 星期六

你就是寫太多測試才會沒時間(1):證明自己的清白

June 17 16:31~18:24

▲圖1:單元測試驗證修改過的email是否正確 

 

前言

Teddy的朋友Kuma幾個月前寫了一本書:《你就是不寫測試才會沒時間:Kuma 的單元測試實戰 -- Java篇》。的確,自從Teddy在N年前開始寫第一個自動化單元測試之後,Teddy一直認為測試是開發不可分割的一部分。好的測試可以協助釐清規格、作為驗收條件、找出回歸錯誤,以及支持重構,讓開發人員走得更穩、更快。

但是,隨著測試案例越來越多,管理與重構這些測試案例就變成另一個頭痛的問題。下周二Teddy舉辦一個網路演講,講題是:「你就是寫太多測試才會沒時間」,就是要討論應對這種現象的方法。這個講題這雖然是一句帶有玩笑性質的話,但也代表對於測試看法的一種演進過程:

不寫測試沒時間 ---> 寫了測試有時間--->  累積太多欠管理的測試又變得沒時間 ---> 下階段是什麼?

針對這個主題,今天談一個單純一點的情況:如何簡單驗證待測程式沒有做它不該做的事情?

***

從範例看問題

軟體測試有一個基本原則:「除了要驗證待測程式做了該做的事情,也要驗證它沒有做不該做的事。」舉個例子,圖1中的User物件,呼叫它的changeEmail方法設定新的email,在第101行中驗證email是否被正確設定。這種測試很常見,但嚴格講起來這個測試並不完整。除了驗證email有被正確修改以外,還需要確保User物件的其他欄位沒有被改變。因為難保changeEmail的實作,除了改變email以外,會不會不小心動到User物件的其他欄位,例如把nickname清空。

但是,如果每一個測試案例都去驗證不應該被改動的欄位真的沒有被異動,將會增加很多測試工作,如圖2所示。


▲圖2:第104~109行驗證User除了email以外的其他欄位維持原狀 

***

這還只是單元測試而已,如果是Use Case層次的測試案例,例如ChangeEmailUseCase,從Repository讀出User之後,理論上相同的assertion還要再寫一次。除了需要花費而外時間撰寫測試,也造成duplication code,增加日後維護測試案例的成本。

***

解決方案

先講結論,Teddy使用AssertJ這個「Fluent assertions for java」的測試工具來解決這個問題。圖3為Teddy使用ezSpec(Teddy自行開發的BDD工具軟體,可以直接用Java寫Given-When-Then,過一陣子會開源)所撰寫的ChangeEmailUseCase測試,第73行透過Repository從資料庫中拿出修改過email的User物件,然後第74行比對email欄位是否被正確修改。

接著在第76~78行使用AspectJ的assertThat做為比對物件的方式,呼叫usingRecursiveComparison,然後透過ignoringFieldsMatchingRegexes指定那些欄位不需要比對,最後再呼叫isEqualTo,就可以排除特定欄位之後,比對兩個物件是否相等。

圖3的程式範例擷取自ezKanban,由於ezKanban支援樂觀鎖,因此在每一個Aggregate物件身上都有一個version欄位用以作為樂觀鎖使用。因為User物件是一個Aggregate,所以User的email改變之後,version數值加1,因此在第77行比對修改前與修改後的兩個User物件實例是否相等的時候,除了排除email,也要排除version。


▲圖3:採用ezSpec撰寫的ChangeEmailUseCase測試 

***

結論

透過工具幫忙,就可以用很簡潔的方式去確保物件的狀態。雖然Teddy在範例中使用AssertJ做為比對的工具,但相信不同的語言應該可以找到類似的工具。如果真的找不到怎麼辦?那就自己寫一個啊。

***

友藏內心獨白:開發人員就是要有Maker精神。

2023年3月27日 星期一

使用ezSpec落實行為驅動開發與實例化需求(10):撰寫自訂報表

March 25 12:44~14:18

▲圖1:ezSpec實作Visitor設計模式類別圖

 

前言

上一集介紹ezSpec的基本報表功能,這一集介紹如何擴充ezSpec,撰寫使用者自訂報表。

***

Visitor設計模式

為了支援自訂報表,ezSpec讓Gherkin keyword實作Visitor設計模式,請參考圖1。在GoF設計模式中,有兩個角色:

  • Element:圖1中的SpecificationElement介面,要接受「拜訪者」的物件需要實做Element介面。該介面有一個accept方法,接受visitor作為參數。該方法的實作會呼叫(callback)visitor.visit,讓visitor可以讀取element的資料。
  • Visitor:圖1中的SpecificationElementVisitor介面,想要拜訪各個Element的具體拜訪者(concrete visitor)需要實作該介面,將拜訪者的邏輯寫在visit方法裡面。在ezSpec中,客製化報表就是一個具體拜訪者。

 

圖2為Feature實作SpecificationElement介面的程式碼,第14行Feature先將自己傳給visitor,接著再將它身上每一個Story傳給visitor。由於Feature是整個Gherkin keyword中最外層的結構,只要拜訪Feature,就能夠拜訪整個Feature file裡面所有的元素,包含Scenario、Scenario、Background與Step。



▲圖2:ezSpec的Feature實作SpecificationElement的程式碼

 

***

 

實作SpecificationElementVisitor產生報表

ezSpec內建的文字檔報表,就是使用上述介紹的機制所產生,請參考圖3。第20行到60行的visit方法,就是用來「拜訪」Feature內的所有元素,產生報表的邏輯。

  

▲圖3:ezSpec內建產生文字報表的Visitor程式碼

 

***

客製化報表範例

開發好ezSpec之後,Teddy就拿它來描述ezKanban的規格。圖3為ezKanban的MoveLane使用案例的規格,用ezSpec的Scenario Outline撰寫。這個功能是將看板系統中,工作流程內部的某一個Lane複製到另一個地方。在描述規格的時候,直接以表格描述複製之前與複製之後Workflow的內容,其執行結果所產生的文字報表如圖4。


▲圖4:以ezSpec所撰寫的ezKanban系統之MoveLane使用案例規格 

 

圖5的內容不太容易閱讀,因為規格中表格還包含著另一個表格,因此Teddy想:「既然MoveLane使用案例是看板視覺化業務邏輯的其中一個功能,為什麼不用視覺化的方式來表達這個功能?」要怎麼用是視覺化方式產生報表,幫它寫一個Visitor吧,請參考圖5。


▲圖5:執行圖3所產生的ezSpec內建文字報表 

 

Teddy使用Markdown的Mermaid擴充(外掛)來繪製Workflow,圖6的WorkflowMermaidVisitor與ezSpec內建的PlainTextVisitor類似,差別在於前者針對MoveLane規格產生Markdown報表,後者則是標準的文字報表。


▲圖6:撰寫WorkflowMermaidVisitor產生MoveLane使用案例的專屬報表 

 

圖7報表的內容與圖4相同,前者以圖形顯示,後者以文字顯示。在MoveLane使用案例的情境中,以圖形顯示比較容易閱讀。


▲圖7:執行圖3所產生的客製化Markdown報表 

 

只要將寫好的Visitor在驗收測試的afterAll()方法中去拜訪Feature,再將結果寫入檔案中,就可以在每次執行驗收測試之後產生新的客製化報表,請參考圖8。


▲圖8:針對MoveLane使用案例驗收測試產生客製化報表

***

結論

寫到這裡剛好第十集,也把ezSpec目前的功能介紹完畢。Teddy預計在2023年七月之後開兩門新課:

  1. Clean TDD(整潔測試驅動開發)
  2. Living Documentation in Agile Development(敏捷開發中的活文件)

在課程中(特別是第二門課)會以ezSpec為範例,在開課前Teddy會先開源ezSpec讓有興趣的鄉民們使用,敬請期待。

***

友藏內心獨白:寫完文件要回頭寫Code。

2023年3月26日 星期日

使用ezSpec落實行為驅動開發與實例化需求(9):內建報表

March 25 10:43~11:48


▲等待報表功能全部完成,ezSpec就可以開源了

 

前言

前幾集介紹ezSpec的基本功能以及同步執行功能,今天介紹ezSpec最後一個功能:報表。報表功能是做到Living Documentation(活文件)的核心功能之一,先用ezSpec描述系統行為,然後這些可執行規格變成驗收條件,執行結果透過報表整理,產出與系統現狀同步的Living Documentation。

目前ezSpec支援產生個別Feature file執行結果的報表,報表格式為txt檔與json檔,整合性報表還在開發中。ezSpec支援Visitor設計模式,開發人員也可以自行撰寫Visitor,產生使用者自訂報表。本集先介紹如何產生內建報表,下一集介紹如何擴充ezSpec,撰寫使用者自訂的報表。

***

產生報表

要讓ezSpec產生報表,首先測試案例(Test Class)要實作EzSpec介面,請參考圖1。

 

▲圖1:測試案例實作EzSpec介面

 

EzSpec介面程式碼如圖2,它身上有四個Annotation:

  • @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class):這是JUnit 5的內建annotation,在JUnit報表中會將test mehtod的名字的底線取代為空白,以方便閱讀。例如,將scenario_using_EzScenario_annotation顯示為scenario using EzScenario annotation。
  • @ExtendWith(EzSpecReportExtension.class):@ExtendWith是JUnit 5支援使用者自行擴充功能的annotation,ezSpec的EzSpecReportExtension透過實作JUnit 5 的AfterAllCallback介面,讓JUnit執行每一個Test Clase之後呼叫ezSpec,以便產生報表。
  • @EzSpecReportFormat({EzSpecReportFormat.Format.json, EzSpecReportFormat.Format.txt}):這是ezSpec自訂的annotation,用來表示產生那些格式的內建報表。目前ezSpec支援txt與json這兩種報表格式
  • @Tag(EzSpecTag.LivingDoc.EzSpec):這也是JUnit 5內建的annotation,用來分類測試案例。參考圖3,ezSpec內建EzSpecTag介面,裡面包含常用的Tag。將Test Class(在ezSpec的情境中,也就是Feature file)用Tag分類,主要以下兩個目的:
    • 在test suite中作為測案案例的選擇條件,例如只挑選單元測試,或是只挑選整合測試。
    • 作為Living Documentation的分類標籤,例如產生Use Case的報表,或是產生在Staging環境測試執行結果的報表。


▲圖2:EzSpec介面程式碼

 

▲圖3:EzSpecTag介面程式碼片段

 

只要測試案例(Test Class)實作EzSpec介面,並依據前幾集介紹的方式撰寫Feature, Story, Scenario, Scenario Outline,執行測試案例之後,ezSpec就會在預設的目錄 target/ezSpec-report 產生報表,請參考圖4。

 

▲圖4:ezSpec在預設目錄中產生報表

 

圖5與圖6分別展示ezSpec產生的純文字檔報表與json格式報表。


▲圖5:ezSpec純文字檔報表範例

 

▲圖6:ezSpec JSON格式報表範例

 

***

不要產生報表

有時候你可能會臨時不要產生某個測試案例的報表,此時可以直接在測試案例上面加上@DisableEzSpecReport。

 

▲圖7:加上@DisableEzSpecReport就不會產生該測試案例的報表

 

如果想要禁止ezSpec產生所有測試案例的報表,可以設定EZREPORT環境變數,把它設為OFF即可。什麼時候會取消報表產生功能?因為產生報表需要時間,也需要寫入檔案,因此會讓測試案例跑得比較慢一點。原本這也不是什麼大問題,但如果你的專案採用類似PiTest這種Mutation Test工具,那麼同一個測試案例會被執行N次,而且在這種情況下,開發人員只需要觀看PiTest產出的報表,並不需要ezSpec的報表。此時,關閉ezSpec產生報表功能,可以加速PiTest執行。

 

***

結論

報表系統算是ezSpec的最後一哩路,目前ezSpec提供的報表屬於個別驗收測試(Feature file)執行結果的報表,雖然使用者可以透過下一集介紹的方式擴充ezSpec產生使用者自訂報表,但Teddy還是想在ezSpec中提供基本的整合報表功能。等待整合報表功能完成,Teddy就會將ezSpec開源給有興趣的鄉民們使用。

***

友藏內心獨白:人要衣裝,佛要金裝,報表也是很重要的功能。

2023年3月25日 星期六

使用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產生報表的功能。

***

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

2023年3月24日 星期五

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

March 23 21:08~22:20


▲第七集了,快到山頂了沒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。


▲圖1:ezSpec的Background範例

***

 

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


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

 

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


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

▲一個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。

2023年3月22日 星期三

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

March 21 09:47~12:28

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

 

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

 

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

 

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

 

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


▲圖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才知道要如何將這些變數替換成測試資料中實際的數值。

 

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

 

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


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

 

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

 

 

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


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

 

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


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

 

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

***

結語

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

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

***

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