Cucumber讀書筆記

Ant發表於2020-04-06

最近讀了一本《Cucumber行為驅動開發指南》,有些收穫想總結下來。

極限程式設計產生了TDD(Test-Driven Development)實踐,然後在TDD的基礎之上又衍生了BDD(Behavior-Driven Development),標準化了那些TDD實踐者的良好習慣。產生的原因背景:領域專家使用他們的行話,技術團隊成員則擁有自己的、專門從設計角度討論領域的語言,由於語言方面的分歧,領域專家描述需求的時候非常模糊,開發人員努力嘗試理解一個全新的領域,卻只能得到模糊的結果。


通過整個團隊、專案涉及的所有人使用同一種大家都能理解的通用語言,就能解決這個問題。Cucumber為存在語言分歧的雙方提供了可以會合的場所,從而促進了通用語言的發現和使用。它是一個工具、或者可以說是一套理念,用近似自然語言的方式來描述系統/產品的真實行為,開發人員可以利用這份描述來做驗收測試,驅動他們寫出行為正確的系統程式碼,而產品、股東等也可以利用這份描述來確定他們希望系統做哪些正確的事情,再則客戶也可以閱讀這份描述來理解這是否是他們需要的產品,最後,測試人員可以使用這種描述來作為測試用例,甚至是可以自動執行的自動化場景。總之,所有人看到的都是同一份描述,同一段文字,沒有歧義,沒有隔閡,不存在誤解(當然前提是這段文字寫得精確又不冗餘,這當然是需要通過磨合鍛鍊溝通才能達到的效果),先來看一段這樣的文字:

Scenario: Successful withdrawal from an account in credit
  Given I have $100 in my account #the context
  When I request $20 #the event
  Then $20 should be dispensed #the outcome
  And I should have $80 in my account now

Cucumber是一個命令列工具,執行的時候它會從普通語言編寫的稱為Feature的文字檔案中讀取你的規格說明、解析需要執行的Scenario,然後針對系統執行Scenario裡面定義的一系列Step,這段文字必須遵循一些最基本的語法規則,這套規則就叫做Gherkin。


筆者這裡就不再介紹更多關於Cucumber基礎的內容,由於之前也有過Cucumber的實踐,書中提到了一些經驗和教訓值得每個使用Cucumber的人反思:

  • Cucumber在解析執行Step的時候實際並不關心你使用哪個關鍵字:Given/Then/When/And/But,他們只不過是為了增加Scenario可讀性而存在的。
  • 我們設計的每個Scenario必須是有獨立意義的,且能夠不依賴其他任何場景獨立執行。
  • 注意Scenario的命名,因為除非很有必要,否則沒有人會想閱讀每個場景的所有步驟,把Scenario當做程式的一個方法名,儘量做到讓人不讀方法內部的程式碼就能知道這是幹什麼的。(建議:不要把Then部分相關的內容引入到Scenario名字中去)
  • 如果你感到Step使用的描述方式出現了一些細微的模糊性,即不同人看了可能會有不同的理解,那麼就開始改善你的用詞,提高用詞的精確性,並跟不同背景、部門的人諮詢反饋和建議。畢竟我們最終目的是達到描述所傳達的資訊是一致的。
  • 每個Feature檔案都可以有一個Background元素,它必須出現在Scenario的前面,它可以通過一些通用的步驟降低Scenario裡Step的重複性,但應該儘量保持Background小節簡短並鮮明生動。這樣增加了故事性,更易於讀書瞭解內容和資訊。
  • 避免將太多如清除快取、開啟伺服器或者開啟瀏覽器之類的技術細節放入背景中。
  • 當你的Scenario時而成功,時而執行失敗的時候,我們稱之為閃爍的場景,針對這種場景,必須努力搞清楚它發生的原因,通常可能是由共享的環境、滲漏的場景或者競爭條件和打瞌睡的步驟所導致的。必須儘快的修復它,否則寧可講整個場景刪除。
  • 通常真正使用Cucumber來做驗收測試和設計場景用例的時候,容易陷入到繁瑣的細節步驟中,例如點選了某按鈕、填寫了某欄位、重新整理了某頁面……這種針對使用者介面控制元件的描述屬於泛化且較低層次的領域描述,應該對其進行合理的抽象提高描述的領域層次。否則,相信我,沒有人會有興趣來閱讀你大段的介面操作!
  • 在大多數團隊中,對於測試的維護都不會出現在靠前優先的位置,這種對待測試的態度是完全錯誤的。團隊需要一絲不苟的關注並維持測試的健康,舉個例子:自動化測試對於依賴自動測試的團隊來說就相當於心跳。豐田車間裡有個著名的故事叫做:停掉生產線。當每次出現問題的時候,車間裡的每位工人都有權力停掉整條生產線,問題馬上會得到所有經驗豐富的員工給予全力關注,只有解決了生產線才會重新啟動,之後再有一個小組專門來繼續分析根源在哪裡。當這個想法第一次提出的時候,只有少數經理贊同,但經過一段時間的實踐,停掉生產線的次數越來越少,實施了這一策略的經理們發現車間的生產量在經歷了短暫的下滑之後,迅速大大超越了那些沿用舊制度的車間。
  • 許多團隊擔心Cucumber總是在描述一些最簡單的場景,其實任何Bug都可以追溯到我們為那部分系統編寫的Cucumber場景的不足上,存在Bug不是任何人的錯誤,而只是一開始我們沒有預料到的一個邊界條件而已。
  • 如果你在編寫自動化測試,你就是在開發軟體,如果你足夠重視測試,並且在第一時間編寫測試,那麼你就會希望將來能回頭再修改這些測試。也就是說,我們在編寫可維護軟體時遵循的那些好習慣在編寫測試程式碼的時候同樣適用。
  • 在複雜的系統中,某些Step的執行必須要跟另外的機器或者系統模組通訊,此時採用監聽事件的方式是讓測試與非同步系統保持同步最快且可靠的手段。(相對於簡單的sleep或者wait固定的時間來說)


從業務的角度Cucumber提供了一種方式來讓整個系統的所有相關人員看同一段文字達到無障礙的理解系統最正確的行為。從技術的角度,Cucumber只是一個命令列工具,把場景中的每一條Step通過正規表示式匹配的方式對映到底層程式碼的實現中去,從而完成自動化測試。至於底層程式碼的實現,Cucumber支援多種語言:Ruby,Java,Javascript等等,都可以在開源社群找到相應的庫支援Cucumber的整套理念。例如下面這段Java程式碼,就對應了幾條Step實現的操作:

@Given("^a shopping list:$")
    public void a_shopping_list(List<ShoppingItem> items) throws Throwable {
        for (ShoppingItem item : items) {
            shoppingList.addItem(item.name, item.count);
        }
    }

    @When("^I print that list$")
    public void I_print_that_list() throws Throwable {
        printedList = new StringBuilder();
        shoppingList.print(printedList);
    }

    @Then("^it should look like:$")
    public void it_should_look_like(String expected) throws Throwable {
        assertEquals(expected, printedList.toString());
    }

對應的場景(Scenario)如下:

 Scenario: Print my shopping list
  The list should be printed in alphabetical order of the item names

    Given a shopping list:
      | name  | count |
      | Milk  | 2     |
      | Cocoa | 1     |
      | Soap  | 5     |
    When I print that list
    Then it should look like:
      """
      1 Cocoa
      2 Milk
      5 Soap

      """

可以看到Cucumber執行的時候不關心關鍵字用的Then/Given/When,當Step的語句能唯一被正規表示式識別的時候,就會投射到相應的Java方法來自動化的做實際的操作。關鍵字的作用永遠只是為了增加場景描述的可讀性而已。


鑑於之前經歷過的Cucumber實踐,筆者看到的問題是:

  • 我們經常把介面的每一個操作步驟給Step化,讓整個Scenario充滿了"I click xxx", "I fill form with xxx", "I login with account xxx"之類的Step,可能一個場景寫下來就有100行之多,相對複雜的系統這樣的場景可能有幾百甚至上千個,沒有人能提起興趣去閱讀成篇的GUI操作步驟而仍然不知道系統在做什麼,只知道當你完成這些點選和表單填寫之後,GUI上會發生什麼事情。所以我們犯的錯誤是描述的領域層次太低,方向就走錯了!
  • 我們經常會看到大量重複的步驟,例如“login xxx”和"fill xxx form"等等,當你發現冗餘的程式碼非常多的時候,重構和抽象肯定是做得不好的。實際上Cucumber有許多方手段支援Scenario步驟的抽象,例如Data Table,Transform,Background, After/Before等Hook,還有Env、World這樣的全域性物件可以用來預定義一些全域性資料和狀態,善用它們可以使得你的Scenario Step言簡意賅,更像一個生動的故事,可以讓讀的人不覺得厭煩而且能快速理解裡面的資訊。
  • 我們經常看到Scenario之間存在依賴性,當一個場景不通過,可能導致數十個場景必然失敗,這說明對於場景的設計是非常脆弱的。這裡必須提到設計場景的人可能是資深的QA,他們並沒有程式碼重構和可維護性的經驗,實現場景的可能是測試開發人員或者開發,它們也可能缺乏對產品功能的足夠深入的瞭解,去避免場景之間的依賴性。只有協作描述的方式才能解決這類問題,甚至在Step的用詞方面,應該早期就把產品人員、運營人員、開發一起拉進來,大家約定通過一致的用詞和句型,把誤解的可能性降到最低。(這也說明了為什麼測試在需求階段就介入的意義何在,實際上那個時候是沒有實際的東西可測的,但早期就從測試基礎建設上做投入,持續的迭代改進必然是最科學有效的方式)
  • 我們的測試人員在寫Step的時候,通常會由於大家的語法、句型習慣不一樣而寫出不同風格的描述Step,有的Step會唯一的對映到一個方法的正規表示式上,有的卻會同時匹配兩三個方法的正規表示式,此時破壞了Step的唯一性和全域性性,Cucumber是不會知道該用哪個正規表示式來匹配你的Step,無法繼續執行下去。由此可見,對正規表示式的設計也是非常講究的,首先它的實現肯定是測試開發人員,很可能不是設計場景的測試人員,但是隻有資深有經驗的測試人員,非常瞭解系統的行為,才知道用哪一套句型可以精準完整的描述出系統所有的行為。這個時候,我們本末倒置的是所有的Step正規表示式都有測試開發人員直接設計並實現,然後測試人員直接拿來使用,結果就感覺到越用越不好用。
  • 由於Cucumber是順序執行的,在解決了場景之間的依賴性之後,複雜的系統可能有幾百甚至上千個場景,從頭跑到尾需要數小時才能得到測試結果的報告。這對於需要快速響應和反饋的敏捷流程來說,往往是不可接受的。開發可能不願意等結果就繼續提交程式碼,從而觸發新一輪的測試。這種時候,我覺得應該從場景的劃分和管理、執行上,就開始重構,把場景按模組、按優先順序、按時間量級進行分類管理(實際上Cucumber也已經提供了許多手段幫助你做到這一點),然後在執行上,可以同時在幾臺不同的虛擬機器上並行測試,這一切在執行之前,就應該評估出完成測試的時間和範圍這些資料,然後由人的經驗來判斷哪些場景需要,哪些這輪不需要,對於程式碼模組劃分較好、敏捷迭代比較成熟的團隊,沒有程式碼修改的模組通常需要極少的測試,這些通過配置就可以輕易解決,讓整個自動化測試流程可以更快的響應得到結果,從而指導開發及時的發現問題改善程式碼質量、修復Bug。可惜,在當時,整個團隊做得並不好!


最後,筆者想強調的是,Cucumber的目的是好的,但是在如今的行業現狀內,並不是每個公司,每個專案都適用的。不要永遠用理想化的理念去對映現實,哪怕這些理念永遠是對的。比如,在有的團隊,即使你的場景設計得再好,故事再生動,可能企業文化風格就決定了測試以外的其他部門根本不會來閱讀它;再比如在有些專案中,產品的行為要牽連到許多硬體、或者難於用Step實現程式碼來自動化控制的東西上,此時Scenario的唯一意義也就淪為了測試用例而已,它無法或者很難被自動化;還有的產品本身存在許多遺留程式碼,亂而且不規範,在高度耦合的情況下,要設計出獨立執行不依賴的場景本身就相當困難,這種時候你甚至要反思Testing或者Bahavior到底能不能驅動開發。

Anyway,我們不能否則Cucumber提出了一種DevOps和Agile裡也提倡的方式,如果用得好,是能夠大大改善產品的質量和整個研發效率的!


如果對Cucumber感興趣,可以去它的GitHub站點找到各種語言版本的支援和原始碼、文件:https://github.com/cucumber