我們現在處理好保存狀態的機制,目前還剩下 POST /api/checkout
的實作還沒加入到裡面,除此之外每次開啟前端時也無法看到最新的購物車狀態,我們要來將這些情境處理到可用的情形。
結帳機制
我們目標的結帳機制是實作「清空購物車」來實現目標,並且在購物車沒有商品或者不存在時顯示錯誤,因此實作上並不困難。
先針對結帳的行為加入對應的 Cucumber 測試,開啟 features/cart.feature
。
1#language:zh-TW
2# ...
3場景: 購物車沒有商品時,當我嘗試結帳會看到「結帳失敗」
4 當 進行結帳
5 那麼 我會看到 JSON 物件包含 "text" 為 "結帳失敗" 的文字
6
7 場景: 購物車有一項商品時,當我嘗試結帳會看到「結帳成功」
8 當 把商品編號 1 加入購物車
9 並且 進行結帳
10 那麼 我會看到 JSON 物件包含 "text" 為 "結帳成功" 的文字
因為這些是新的步驟,修改 features/step_definitions/common.rb
增加新的步驟定義
1# ...
2When('進行結帳') do
3 post '/api/checkout'
4end
5
6Then('我會看到 JSON 物件包含 {string} 為 {string} 的文字') do |key, value|
7 json = JSON.parse(last_response.body)
8
9 expect(json).to include(key => value)
10end
完成測試的實作後,修改 app.rb
加入結帳的邏輯。
1# ...
2module Shop
3 class API < Grape::API
4 CHECKOUT_SUCCESS = { text: '結帳成功' }.freeze
5 CHECKOUT_FAILED = { text: '結帳失敗' }.freeze
6 # ...
7 namespace :api do
8 # ...
9 post '/checkout' do
10 @cart = Cart.find(1)
11 next CHECKOUT_FAILED if @cart.empty?
12
13 @cart.destroy
14 CHECKOUT_SUCCESS
15 rescue ActiveRecord::RecordNotFound
16 CHECKOUT_FAILED
17 end
18 end
19 end
20end
因為我們沒有要串接金流或者進行其他的處理,只需要呼叫 #destory
方法即可,然而在預設的行為下 ActivRecord 不一定會幫我們移除 CartItem
的內容,因此我們還需要修改 Cart
的實作,開啟 models/cart.rb
進行調整。
1class Cart < ActiveRecord::Base
2 has_many :items, class_name: 'CartItem', dependent: :destroy, autosave: true
3
4 # ...
5
6 def empty?
7 items.sum(&:amount) == 0
8 end
9end
我們對 has_many
增加了 dependent: :destroy
選項,這樣在移除購物車的時候就會自然的將 CartItem
一起清理掉,除此之外我們在購物車品項都為 0
的時候要拒絕結帳,因此還需要額外實作一個 #empty?
方法並且統計品項的數量,用來在不正確的數量時回傳錯誤訊息。
最後運行 bundle exec cucumber
確認實作沒有破壞原有的行為,也能夠正常通過 Cucumber 文件所撰寫的要求。
購物車資訊
解決了結帳的行為後,我們需要讓原本的購物車可以重現後端保存的狀態,不然可能會在不知情的狀況下對錯誤的商品進行結帳,因此我們還需要加入 GET /api/cart
來回傳最後的狀態,開啟 app.rb
加入實作。
1module Shop
2 class API < Grape::API
3 CHECKOUT_SUCCESS = { text: '結帳成功' }.freeze
4 CHECKOUT_FAILED = { text: '結帳失敗' }.freeze
5 # ...
6 namespace :api do
7 # ...
8 get '/cart' do
9 Cart.find_or_initialize_by(id: 1).status
10 end
11
12 # ...
13 post '/cart' do
14 # ...
15 @cart.save
16 @cart.status
17 end
18 end
19 end
20end
因為我們都需要將 { 1 => 1 }
這樣的 Hash 回傳,因此統一封裝成一個 #status
方法來反應這個要求,修改 models/cart.rb
擴充行為。
1class Cart < ActiveRecord::Base
2 # ...
3
4 def status
5 items.map do |item|
6 [item.product_id, item.amount]
7 end.to_h
8 end
9end
接著要修改前端的實作,讓購物車載入時就能夠讀取到上一次的狀態,先修改 src/stores/cart.ts
讓我們可以使用 refresh()
方法刷新購物車。
1// ...
2
3export const useCartStore = defineStore('cart', () => {
4 // ...
5
6 return {
7 items,
8 hasItem,
9 add,
10 remove,
11 clear,
12 totalItems,
13 refresh,
14 }
15})
接著修改 src/api.ts
加入新的 API 呼叫。
1// ...
2export const fetchProducts = async () => await fetch(`${API_SERVER}/api/products`).then(res => res.json())
3export const getCart = async() => await fetch(`${API_SERVER}/api/cart`).then(res => res.json())
4// ...
最後到 src/components/Cart.vue
裡面將 getCart()
呼叫來載入新的購物車資訊。
1<script setup lang="ts">
2import { ref, reactive, computed } from 'vue'
3import { useProductStore } from '../stores/product'
4import { useCartStore } from '../stores/cart'
5import { getCart, checkout } from '../api'
6
7// ...
8const cart = useCartStore()
9
10// ...
11getCart().then(items => cart.refresh(items))
12</script>
這樣在開啟前端預覽時就會正常,然而我們的測試並沒有區分是 GET /api/cart
還是 POST /api/cart
因此會失敗,我們還需要修改 features/support/world.ts
裡面對 /api/cart
的定義,在 GET 狀態下回傳空的購物車。
1// ...
2Before(async function() {
3 const state: Record<number, number> = {}
4
5 await this.page.route('/api/cart', async (route: playwright.Route) => {
6 if(route.request().method() === 'GET') {
7 return await route.fulfill({ json: {} })
8 }
9
10 const data = await route.request().postDataJSON()
11
12 state[Number(data.id)] = state[data.id] || 0
13 state[Number(data.id)] += Number(data.amount)
14
15 await route.fulfill({ json: state })
16 })
17})
最後運行 yarn run cucumber-js
就可以看到測試順利通過。
這裡並沒有在舉出「購物車已有物品」的例子,透過前面一系列的實作後,可以挑戰看看如何實現對應的測試來驗證前端的行為是否正常。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 同時完成測試與文件 - 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 的文件測試法