用測試完善規格 - Rails 開發實踐
過去我在寫測試的時候經常會有「這裡該測試嗎?」的疑問,然而這個問題其實可以從另一個角度思考,那就是「這些測試組以完善規則嗎?」去想,以我們第一個 E2E Testing 的測試作為例子,雖然可以通過測試,然而實作的內容只是一些假資料,我們需要用另一條測試從其他角度去驗證,讓實作最終變成我們預期的樣子。
檢查資料
我們之所以會檢查訂閱狀態顯示「30 天後到期」是因為剛訂閱完畢後應該會有這樣的狀況,然而這只需要簡單的寫死即可,為了達成這件事情,我們可以利用檢查資料是否存在的方式做處理。
舉例來說,在 /plans
的頁面原本是「訂閱」按鈕,現在會因為我們已經訂閱,而把這個按鈕隱藏,或者顯示「已經訂閱」的訊息作為替代,也因此我們可以加入一個新的測試案例。
1# features/subscription.feature
2#language:zh-TW
3# ...
4 場景: 當 Aotoki 在訂閱後,再次選擇方案時會看到「已經訂閱」
5 當 我打開訂閱頁面
6 並且 點選 "訂閱"
7 並且 我打開訂閱頁面
8 那麼 我會看到 "已經訂閱"
步驟其實跟訂閱並沒有太大差異,只是我們換到其他頁面去做確認,透過這樣的方式,也可以幫助我們多次驗證一個功能是否有沒有考慮到的地方,也因此我們才會將這些規格以 Key Examples(關鍵案例)的方式進行描述。
狀態管理
到了這一步,是不是終於要建立資料表了呢?很抱歉,資料表的建立還需要再多一些步驟,在這之前我們需要思考的是「狀態」管理的問題。
一個功能要運作,並不一定要依賴資料庫,或者說資料庫是「持久化儲存」的機制,這也代表如果只是要紀錄「訂閱狀態」的話是不需要依靠資料庫,我們只需要讓 Rails 知道某個使用者具備「已訂閱」的狀態即可,因此可以實作一個 Model 來實現這件事情。
1class Subscription
2 class << self
3 # 用一個 Set 資料結構紀錄不重複的使用者 ID
4 def subscribed
5 @subscribed ||= Set.new
6 end
7
8 # 當建立一筆資料會插入 ID 到這個 Set 中
9 def create(user_id:)
10 subscribed << user_id
11 end
12
13 # 以使用者 ID 為基準查詢,如果找到就回傳一個 Subscription 物件
14 def by_user(user_id:)
15 return [] unless subscribed.include?(user_id)
16
17 [Subscription.new]
18 end
19 end
20end
像這樣子,我們就可以在記憶體中保存 Subscription 的狀態,而且不需要連接資料庫,雖然並不能持久化的保存資料,但是足夠讓我們通過這一個新的測試案例。
可能會有人好奇為什麼
#by_user
回傳的是陣列,這是為了讓他再轉換為 ActiveRecord 連接資料庫時,跟scop :by_user, ->(user_id:) { where(user_id:) }
有相同的介面(Interface)的原因。
修改實作
接下來,我們需要將 Controller 和 View 稍作調整,這樣就可以再次通過新加入的測試。
1class SubscriptionsController < ApplicationController
2 # ...
3
4 def create
5 Subscription.create(user_id: current_user.id)
6 redirect_to subscriptions_path
7 end
8end
在 SubscriptionController
加入 Subscription.create(...)
的呼叫,來實現跟 ActiveRecord 建立資料一樣的介面,之後替換成真正的資料庫時就可以省去修改的時間。
1class PlansController < ApplicationController
2 def index
3 @subscription = Subscription.by_user(user_id: current_user.id).first
4 end
5end
至於 PlansController
就需要定義 #index
方法來找到訂閱,這裡我們可以選擇用 current_user.subscriptions
或者 Subscription.by_user(...)
兩個不同的方式,在設計的意涵上是有點不同的,我們之後會再深入討論這個問題。
1<% if @subscription.present? %>
2 <div>已經訂閱</div>
3<% else %>
4 <%= form_with url: subscriptions_path, method: :post do |f| %>
5 <%= f.submit '訂閱' %>
6 <% end %>
7<% end %>
最後我們可以在 View 上面加入 @subscription.present?
的檢查,那麼就可以順利地呈現「已經訂閱」的訊息,到這個階段我們還是不需要考慮「資料庫」的問題。
那麼,什麼時候該用到資料庫呢?在這之前我們需要先針對「脈絡」「資訊」「資料」做一次討論,就會知道該如何正確的應用資料庫。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 前言 - Rails 開發實踐
- 將需求實現的準備 - Rails 開發實踐
- 獲取規格的技巧 - Rails 開發實踐
- 可以測試的規格 - Rails 開發實踐
- 快速通過測試的方法 - Rails 開發實踐
- 用測試完善規格 - Rails 開發實踐
- 資料跟資訊的差異 - Rails 開發實踐
- 用測試資料驗證邏輯 - Rails 開發實踐
- 預期外狀況的檢查 - Rails 開發實踐
- 預期外狀況的測試 - Rails 開發實踐
- 聚合多筆資料 - Rails 開發實踐
- 重構與修正邏輯 - Rails 開發實踐
- 加入聚合實體 - Rails 開發實踐
- 持久化資料 - Rails 開發實踐
- 實體與倉庫 - Rails 開發實踐
- 聚合與邊界 - Rails 開發實踐
- 使用案例與服務 - Rails 開發實踐
- 結語 - Rails 開發實踐