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

用測試完善規格 - Rails 開發實踐

這篇文章是 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? 的檢查,那麼就可以順利地呈現「已經訂閱」的訊息,到這個階段我們還是不需要考慮「資料庫」的問題。

那麼,什麼時候該用到資料庫呢?在這之前我們需要先針對「脈絡」「資訊」「資料」做一次討論,就會知道該如何正確的應用資料庫。