---
title: "用測試完善規格 - Rails 開發實踐"
date: 2023-08-11T00:00:00+08:00
publishDate: 2023-08-11T00:00:00+08:00
lastmod: 2023-09-03T17:33:12+08:00
tags: ["測試","經驗","心得","Rails","Rails 開發實踐"]
series: "rails-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/08/11/rails-in-practice-use-test-to-complete-specification/"
language: "zh-tw"
---


過去我在寫測試的時候經常會有「這裡該測試嗎？」的疑問，然而這個問題其實可以從另一個角度思考，那就是「這些測試組以完善規則嗎？」去想，以我們第一個 E2E Testing 的測試作為例子，雖然可以通過測試，然而實作的內容只是一些假資料，我們需要用另一條測試從其他角度去驗證，讓實作最終變成我們預期的樣子。

<!--more-->

## 檢查資料{#check-data}

我們之所以會檢查訂閱狀態顯示「30 天後到期」是因為剛訂閱完畢後應該會有這樣的狀況，然而這只需要簡單的寫死即可，為了達成這件事情，我們可以利用檢查資料是否存在的方式做處理。

舉例來說，在 `/plans` 的頁面原本是「訂閱」按鈕，現在會因為我們已經訂閱，而把這個按鈕隱藏，或者顯示「已經訂閱」的訊息作為替代，也因此我們可以加入一個新的測試案例。

```gherkin
# features/subscription.feature
#language:zh-TW
# ...
  場景: 當 Aotoki 在訂閱後，再次選擇方案時會看到「已經訂閱」
    當 我打開訂閱頁面
    並且 點選 "訂閱"
    並且 我打開訂閱頁面
    那麼 我會看到 "已經訂閱"
```

步驟其實跟訂閱並沒有太大差異，只是我們換到其他頁面去做確認，透過這樣的方式，也可以幫助我們多次驗證一個功能是否有沒有考慮到的地方，也因此我們才會將這些規格以 Key Examples（關鍵案例）的方式進行描述。

## 狀態管理{#manage-state}

到了這一步，是不是終於要建立資料表了呢？很抱歉，資料表的建立還需要再多一些步驟，在這之前我們需要思考的是「狀態」管理的問題。

一個功能要運作，並不一定要依賴資料庫，或者說資料庫是「持久化儲存」的機制，這也代表如果只是要紀錄「訂閱狀態」的話是不需要依靠資料庫，我們只需要讓 Rails 知道某個使用者具備「已訂閱」的狀態即可，因此可以實作一個 Model 來實現這件事情。

```ruby
class Subscription
  class << self
    # 用一個 Set 資料結構紀錄不重複的使用者 ID
    def subscribed
      @subscribed ||= Set.new
    end

    # 當建立一筆資料會插入 ID 到這個 Set 中
    def create(user_id:)
      subscribed << user_id
    end

    # 以使用者 ID 為基準查詢，如果找到就回傳一個 Subscription 物件
    def by_user(user_id:)
      return [] unless subscribed.include?(user_id)

      [Subscription.new]
    end
  end
end
```

像這樣子，我們就可以在記憶體中保存 Subscription 的狀態，而且不需要連接資料庫，雖然並不能持久化的保存資料，但是足夠讓我們通過這一個新的測試案例。

> 可能會有人好奇為什麼 `#by_user` 回傳的是陣列，這是為了讓他再轉換為 ActiveRecord 連接資料庫時，跟 `scop :by_user, ->(user_id:) { where(user_id:) }` 有相同的介面（Interface）的原因。

## 修改實作{#modify-implementation}

接下來，我們需要將 Controller 和 View 稍作調整，這樣就可以再次通過新加入的測試。

```ruby
class SubscriptionsController < ApplicationController
  # ...

  def create
    Subscription.create(user_id: current_user.id)
    redirect_to subscriptions_path
  end
end
```

在 `SubscriptionController` 加入 `Subscription.create(...)` 的呼叫，來實現跟 ActiveRecord 建立資料一樣的介面，之後替換成真正的資料庫時就可以省去修改的時間。

```ruby
class PlansController < ApplicationController
  def index
    @subscription = Subscription.by_user(user_id: current_user.id).first
  end
end
```

至於 `PlansController` 就需要定義 `#index` 方法來找到訂閱，這裡我們可以選擇用 `current_user.subscriptions` 或者 `Subscription.by_user(...)` 兩個不同的方式，在設計的意涵上是有點不同的，我們之後會再深入討論這個問題。

```html
<% if @subscription.present? %>
  <div>已經訂閱</div>
<% else %>
  <%= form_with url: subscriptions_path, method: :post do |f| %>
    <%= f.submit '訂閱' %>
  <% end %>
<% end %>
```

最後我們可以在 View 上面加入 `@subscription.present?` 的檢查，那麼就可以順利地呈現「已經訂閱」的訊息，到這個階段我們還是不需要考慮「資料庫」的問題。

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

