蒼時弦也
蒼時弦也
資深軟體工程師
發表於

可以測試的規格 - Rails 開發實踐

這篇文章是 Rails 開發實踐 系列的一部分。

當我們有了關鍵案例(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 的功能定義檔案組合起來。