用測試資料驗證邏輯 - Rails 開發實踐
因為我們還沒有完善所有的邏輯,因此儲存到資料庫將狀態持久化的機制可以先不處理,接下來我們需要建構不同的「測試資料」讓訂閱狀態有不同的呈現,而不是像現在這樣只有寫死的訊息。
加入新的測試
現在訂閱狀態只會有「30 天後到期」這樣的訊息,然而實際上應該要隨著天數改變才合理,因此這次我們會加入新的測試讓天數有不同的變化。
1# features/subscription.feature
2#language:zh-TW
3# ...
4 場景: 假設 Aotoki 已經訂閱,並且在 15 天後到期
5 假設 會員 Aotoki 已經訂閱,並且在 15 天後到期
6 當 我打開訂閱狀態頁面
7 那麼 我會看到 "15 天後到期"
這樣一來,除了訂閱當下的 30 天後到期
之外,我們還需要能夠紀錄不同的狀態,另一方面還要考慮到「已過期」的呈現方式,因此我們還要再加入一個在過去時間點到期的測試,永遠顯示「0 天後到期」作為目前的規格。
1# features/subscription.feature
2#language:zh-TW
3# ...
4 場景: 假設 Aotoki 已經訂閱,並且在 15 天前到期
5 假設 會員 Aotoki 已經訂閱,並且在 15 天前到期
6 當 我打開訂閱狀態頁面
7 那麼 我會看到 "0 天後到期"
一切都完成之後,我們可以加入新的步驟定義讓我們可以使用這兩個動作。
1# features/step_definitions/subscription.rb
2
3Given('會員 {word} 已經訂閱,並且在 {int} 天後到期') do |name, amount|
4 Subscription.create(user_id: @users[name], expired_at: amount.days.from_now)
5end
6
7Given('會員 {word} 已經訂閱,並且在 {int} 天前到期') do |name, amount|
8 Subscription.create(user_id: @users[name], expired_at: amount.days.ago)
9end
這裡我們讓 Subscription.create(...)
可以多接受一個 expired_at
參數讓我們可以設定過期時間,接下來就是要修改這些處理。
可能有人會注意到,現在的狀態是保存在記憶體中的,為什麼 Cucumber 使用 Capybara 模擬卻可以正常使用呢?這是因為在沒有特別設定的狀態下,我們並不會開啟真正的瀏覽器,而是模擬 Rack 請求來驗證,如果過早開始加入 JavaScript 或者前後端分離,就會失去這樣的優勢,也會讓測試時間拉長。
修改實作
原本的 SubscriptionController
是寫死的文字,現在我們會從 Subscription
來抓取資料,並且根據抓取到的資料呈現出不同的文字訊息。
1class SubscriptionsController < ApplicationController
2 def index
3 @subscription = Subscription.by_user(user_id: current_user.id).first
4
5 render plain: "#{@subscription.expired_in_days} 天後到期"
6 end
7
8 # ...
9end
這裡我們使用了跟 PlansController
相同的 .by_user
方法,並且呼叫 #expired_in_days
來取得到期時間的文字訊息。然而目前我們的 Model 是無法符合這樣的需求,因此還需要調整 Model 來反映這件事情。
1class Subscription
2 class << self
3 # ...
4
5 def create(**attrs)
6 subscribed << new(attrs)
7 end
8
9 def by_user(user_id:)
10 subscribed.select { |subscription| subscription.user_id == user_id }
11 end
12 end
13
14 # Set 會用 #hash 或 #eql? 檢查是否重複,這裡希望每個使用者都只有一筆資料,因此用 user_id 來處理
15 delegate :hash, to: :user_id
16
17 include ActiveModel::Model
18 include ActiveModel::Attributes
19
20 attribute :user_id, :integer
21 attribute :expired_at, :datetime, default: -> { 30.days.from_now }
22
23 def expired_in_days
24 [0, ((expired_at - Time.zone.now) / 1.day).ceil].max
25 end
26end
修改過後的 Model 因為開始需要紀錄不同的屬性來反應狀態,因此我們將 ActiveModel
混合(Minix)到裡面,這也是為了配合 ActiveRecord 的行為,這樣在之後的重構會變得更加容易。
在這裡要注意的是 Rails 並沒有區分 Repository(倉庫)跟 Entity(實體)這兩個概念,我們會在後續仔細討論差異,在使用的時候要有意識的區分兩者,另一方面
#expired_in_days
實際上跟狀態沒有直接關係而是顯示的資訊處理,在未來會被重構抽離出來,放到 Helper 或者 Presenter(呈現物件)
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 前言 - Rails 開發實踐
- 將需求實現的準備 - Rails 開發實踐
- 獲取規格的技巧 - Rails 開發實踐
- 可以測試的規格 - Rails 開發實踐
- 快速通過測試的方法 - Rails 開發實踐
- 用測試完善規格 - Rails 開發實踐
- 資料跟資訊的差異 - Rails 開發實踐
- 用測試資料驗證邏輯 - Rails 開發實踐
- 預期外狀況的檢查 - Rails 開發實踐
- 預期外狀況的測試 - Rails 開發實踐
- 聚合多筆資料 - Rails 開發實踐
- 重構與修正邏輯 - Rails 開發實踐
- 加入聚合實體 - Rails 開發實踐
- 持久化資料 - Rails 開發實踐
- 實體與倉庫 - Rails 開發實踐
- 聚合與邊界 - Rails 開發實踐
- 使用案例與服務 - Rails 開發實踐
- 結語 - Rails 開發實踐