經過調整成 ViteRuby 的專案結構後,我們已經讓 Vite 所撰寫的前端恢復基本的功能。然而我們使用 Grape 所撰寫的後端行為還無法正常運作,因此接下來我們要用類似的方法將後端重新實現,並且通過所有的 Cucumber 測試。
還原資料表
在使用 Grape 的實作中,我們是利用 ActiveRecord 來實現資料持久化的處理,透過相同的方式,我們這次利用 Rails 的 Database Migration(資料庫遷移)來管理我們的資料表,使用以下命令建構基礎樣板。
1rails generate model product name:string price:integer
2rails generate model cart
3rails generate model cart_item cart:references product_id:bigint amount:integer
因為 CartItem
會需要限制同一個購物車中只能放入一次相同的商品,因此我們需要在 db/migrations
目錄下找到對應的 Migration 檔案,並且使用 t.index
增加索引。
1class CreateCartItems < ActiveRecord::Migration[7.0]
2 def change
3 create_table :cart_items do |t|
4 t.references :cart, null: false, foreign_key: true
5 t.bigint :product_id, null: false
6 t.integer :amount, default: 0
7
8 t.timestamps
9
10 t.index %i[cart_id product_id], unique: true
11 end
12 end
13end
接下來運行 rails db:migrate
將資料表建立起來,我們要先讓 Cucumber 的步驟定義能夠還原測試資料的建立行為。
修改 features/step_definitions/common.rb
加入新的步驟定義,產生商品。
1# ...
2Given('這裡有一些商品') do |table|
3 table.hashes.each do |attrs|
4 Product.create!(**attrs)
5 end
6end
還原行為
我們在 Model 上也有一些行為的實作,繼續修改產生出來的 app/model/cart_item.rb
來增加對應的行為。
1class CartItem < ApplicationRecord
2 belongs_to :cart
3
4 def adjust(count)
5 self.amount = [amount + count, 0].max
6 end
7end
繼續調整 app/model/cart.rb
將之前實作的行為整合進來。
1class Cart < ApplicationRecord
2 has_many :items, class_name: 'CartItem', dependent: :destroy, autosave: true
3
4 def update_amount(product_id, count)
5 item = items.find { |i| i.product_id == product_id } || items.build(product_id:)
6 item.adjust(count)
7 end
8
9 def empty?
10 items.sum(&:amount) == 0
11 end
12
13 def status
14 items.map do |item|
15 [item.product_id, item.amount]
16 end.to_h
17 end
18end
因為本身就是繼承 ActiveRecord 的實作當作基礎,我們基本上不太需要做太多的修改就能夠讓 Model 的行為恢復,接下來修改 Controller 實際使用資料庫的資料。
打開 app/controllers/api/products_controller.rb
從資料庫載入商品資訊。
1module Api
2 class ProductsController < ActionController::API
3 def index
4 render json: Product.all
5 end
6 end
7end
接著修改 app/controllers/api/carts_controller.rb
讓購物車的商品可以正確的被更新以及計算。
1module Api
2 class CartsController < ActionController::API
3 def show
4 render json: Cart.find_or_initialize_by(id: 1).status
5 end
6
7 def create
8 @cart = Cart.find_or_initialize_by(id: 1)
9 @cart.update_amount(params[:id], params[:amount])
10 @cart.save
11
12 render json: @cart.status
13 end
14 end
15end
最後是 app/controllers/api/checkouts_controller.rb
實現結帳的行為。
1module Api
2 class CheckoutsController < ActionController::API
3 CHECKOUT_SUCCESS = { text: '結帳成功' }.freeze
4 CHECKOUT_FAILED = { text: '結帳失敗' }.freeze
5
6 def create
7 @cart = Cart.find(1)
8 return render json: CHECKOUT_FAILED if @cart.empty?
9
10 @cart.destroy
11 render json: CHECKOUT_SUCCESS
12 rescue ActiveRecord::RecordNotFound
13 render json: CHECKOUT_FAILED
14 end
15 end
16end
基本上跟 Grape 的「核心邏輯」沒有太大的差異,主要是我們需要調整為使用 render json: @cart.status
的方式來指定使用 JSON 渲染內容,假設我們有額外切分一個 Use Case 類型的物件,還能夠更清楚地做出區隔。
重現測試步驟
因為原本的前端測試是用 TypeScript 的 Playwright 框架所實現,因此我們需要將這些步驟轉換成 Rails 常用的 Capybara 方式實作,才能夠正確的讓測試進行。
修改 features/step_definitons/common.rb
補齊缺少的測試。
1# ...
2
3When('把 {string} 加入購物車') do |product_name|
4 within '[data-testid^="product"]', text: product_name do
5 click_on '加入購物車'
6 end
7end
8
9When('把 {string} 移出購物車') do |product_name|
10 within '[data-testid^="product"]', text: product_name do
11 click_on '移出購物車'
12 end
13end
14
15When('進行結帳') do
16 click_on '結帳'
17end
18
19Then('可以看見購物車商品數為 {int}') do |amount|
20 expect(page.find('[data-testid="cart-amount"]')).to have_text("一共 #{amount} 項商品")
21end
22
23Then('可以看見購物車總金額為 {string}') do |subtotal|
24 expect(page.find('[data-testid="cart-subtotal"]')).to have_text("總金額為 #{subtotal}")
25end
26
27Then('可以看見結帳結果為 {string}') do |result|
28 expect(page).to have_text(result)
29end
實現的邏輯跟使用 Playwright 的概念類似,我們先利用 Finder
類型的操作,找出我們想要的元素,並且以此為基礎來進行互動,或者檢查文字內容。
Capybara 的
Finder
可以用within
來限定範圍,或者page.find(...)
來選定特定元素。
最後,我們確保 ./bin/dev
的模式下有重新建置前端的 Assets 後,運行 bundle exec cucumber
來確定我們已經將原本的行為全部都重現完畢。
到此為止,我們可以看到 Cucumber 除了能夠呈現文件的效果之外,在轉換語言、框架的前提也能夠變成驗證擁有相同行為的工具,這讓我們在開發的選擇上也獲得更大的彈性,更容易根據當下的需要調整架構。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 同時完成測試與文件 - Cucumber 的文件測試法
- 基本語法:功能描述 - Cucumber 的文件測試法
- 基本語法:驗證行為 - Cucumber 的文件測試法
- 基本語法:步驟定義 - Cucumber 的文件測試法
- 基本語法:輔助設定 - Cucumber 的文件測試法
- 前端環境:Vite 與 Cucumber - Cucumber 的文件測試法
- 商品列表與加入購物車 - Cucumber 的文件測試法
- 重構與移出購物車 - Cucumber 的文件測試法
- 商品資料與總價 - Cucumber 的文件測試法
- 結帳與結果 - Cucumber 的文件測試法
- 整理前端實作 - Cucumber 的文件測試法
- 初始化後端專案 - Cucumber 的文件測試法
- 商品資料 API - Cucumber 的文件測試法
- 更新購物車 API - Cucumber 的文件測試法
- 加入資料模型 - Cucumber 的文件測試法
- 持久化保存 - Cucumber 的文件測試法
- 結帳處理 - Cucumber 的文件測試法
- 在 Rails 的前後端分離 - Cucumber 的文件測試法
- 匯入前端實作 - Cucumber 的文件測試法
- 重現後端實作 - Cucumber 的文件測試法
- 累積價值 - Cucumber 的文件測試法