當我們有了關鍵案例(Key Examples)後,規格已經相對的明確,如果還能夠被自動化測試的話,是不是一件更好的事情呢?我們可以利用 Cucumber 來實踐 E2E Testing(端對端測試)讓我們模擬使用者實際操作來驗證規格的完善。
意外的快
相對於 Unit Test(單元測試)過去我對 Cucumber 的印象就是準備起來非常耗時,這是因為我們需要寫成一般人可以閱讀的文件,一步一步的描述操作,然後再去實現每一個步驟的動作,然而熟悉之後其實比想像中還更快。
實踐測試最為困難的一個地方,其實是「知道該測什麼」也就是當你不清楚要對什麼做測試的時候,會感受到「這樣做是否有用嗎?」的感覺,也很難直覺地寫出測試,然而從規格下手,測試的目的就非常的明確,其實不用思考太多就能夠完成定義。
當 E2E Testing 的測試完成後,我們逐步拆解的物件其實就是單元測試的對象,如此一來在重構過程中補上單元測試,就很自然的將一個完整的測試加入到軟體中,隨著測試的增加反而會覺得開發速度越來越快速,因為很多需要手動檢查或者「邏輯卡關」的部分都會被測試阻擋下來。
基本語法
這系列的文章不會特別說明語法的應用,我會在未來新增一系列針對 Cucumber 測試的文章來補足這部分,這次我們會在文章中將差例子來作為示範。
想要實現符合「蒼時在 2023-01-01 訂閱後,畫面上會顯示 2023-01-30 到期的訊息」這條規格,我們可以像這樣撰寫。
1# features/subscription.feature
2# language:zh-TW
3功能: 會員訂閱
4 背景:
5 假設 這裡有一些會員
6 | name | email |
7 | Aotoki | [email protected] |
8 並且 作為使用者 Aotoki 登入
9
10 場景: 當 Aotoki 在當下進行訂閱,會看到 30 天後到期的訊息
11 當 我打開訂閱頁面
12 並且 點選 "訂閱"
13 並且 我打開訂閱狀態頁面
14 那麼 我會看到 "30 天後到期"
在上面這個 Cucumber 的規格定義中,我們會看到一些關鍵字,像是 功能
、背景
這類,這些是 Cucumber 用來描述行為跟步驟的關鍵字,剩下的部分基本上都是自訂的「步驟」像是 這裡有一些會員
和 點選 "訂閱"
等等。
以下是關鍵字的簡介,詳細的資訊可以參考 Cucumber 文件的說明。
關鍵字 | 說明 |
---|---|
功能 | 定義是什麼功能,建議細一點。如:「新增文章」 |
背景 | 所有場景執行之前要先執行的步驟 |
場景 | 可以視為對應一條 Key Examples,描述想要得到怎樣的效果 |
假設 | 執行動作之前的測試資料準備,像是預先建立好的會員、訂閱方案 |
當 | 實際上執行的動作,通常是使用這個功能的行為 |
那麼 | 結果,驗證動作執行完畢後有得到對應的結果 |
並且 | 連接詞,如果這個步驟跟上一個相同就可以使用 |
這裡在規格測試時把日期(2023-01-30)改為相對時間 30 天前,這是因為 E2E Testing 是模擬真實使用者操作,比起修改到「指定時間」的造假方式,更應該選擇不需要模擬的方案,同時 30 天後到期也更能反應「每次延長 30 天」的規格,如果沒有實際撰寫可能不會發現這個情況。
步驟定義
因為我們讓規格可以用人類能夠理解的方式定義,因此需要讓這些文件的步驟可以被程式執行,Cucumber 會幫助我們解析裡面有意義的關鍵字作為參數(Parameter)然而我們還是需要撰寫對應的描述。
1# features/step_definitions/subscription.rb
2
3Given('這裡有一些會員') do |table|
4 @users ||= {}
5 @passwords ||= {}
6
7 table.hashes.each do |row|
8 name = row.delete('name')
9 @passwords[name] = Devise.friendly_token[0..20]
10 @users[name] = User.create!(
11 row.merge(password: @passwords[name])
12 ).id
13 end
14end
基本上不太需要依靠 FactoryBot 來建立資料,雖然這樣寫起來有點不太好看,然而大多數時候是足夠使用的,一些會被參考的物件,可以先用實例變數暫存起來。
在 Cucumber 的設計中,整個測試是一個世界(World)裡面會有許多不同的場景,也因此跟 Ruby 常見的 RSpec 每次都重設所有測試環境不太一樣,實例變數(Instance Variable)會是共通的,不過我們還是可以透過一些簡單的處理清除掉測試資料。
1# features/step_definitions/subscription.rb
2
3# ...
4
5Given('作為使用者 {word} 登入') do |name|
6 visit new_user_session_path
7 fill_in 'user_email', with: User.find(@users[name]).email
8 fill_in 'user_password', with: @passwords[name]
9 click_on '登入'
10end
11
12When('我打開訂閱頁面') do
13 visit plans_path
14end
15
16When('我打開訂閱狀態頁面') do
17 visit subscriptions_path
18end
19
20When('點選 {string}') do |text|
21 click_on text
22end
23
24Then('我會看到 {string}') do |text|
25 expect(page).to have_text(text)
26end
其他的步驟以此類推去實作,雖然 Cucumber 有用中文實作像是 當('我打開訂閱頁面')
這樣的方法,但還是會推薦使用英文撰寫,步驟內的內容大多是以 RSpec 和 Capybara 為基礎的,這是因為 Cucumber 是以 RSpec 為基礎拓展,某方面來說可以視為 RSpec 的延伸。
稍微習慣 Cucumber 後,其實會發現只是將以往寫 RSpec 的部分重新改寫成數個小步驟,然後透過 Cucumber 的功能定義檔案組合起來。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 前言 - Rails 開發實踐
- 將需求實現的準備 - Rails 開發實踐
- 獲取規格的技巧 - Rails 開發實踐
- 可以測試的規格 - Rails 開發實踐
- 快速通過測試的方法 - Rails 開發實踐
- 用測試完善規格 - Rails 開發實踐
- 資料跟資訊的差異 - Rails 開發實踐
- 用測試資料驗證邏輯 - Rails 開發實踐
- 預期外狀況的檢查 - Rails 開發實踐
- 預期外狀況的測試 - Rails 開發實踐
- 聚合多筆資料 - Rails 開發實踐
- 重構與修正邏輯 - Rails 開發實踐
- 加入聚合實體 - Rails 開發實踐
- 持久化資料 - Rails 開發實踐
- 實體與倉庫 - Rails 開發實踐
- 聚合與邊界 - Rails 開發實踐
- 使用案例與服務 - Rails 開發實踐
- 結語 - Rails 開發實踐