之前因為使用 Playwright 的方式造假後端 API 造成前端的實際畫面是無法使用,接下來在後端的部分我們要將商品 API 完成一個雛形讓前端可以恢復正常。
在實際的開發流程中,前後端確認完畢 API 的資料結構後會同步進行,我們切分成兩個段落因此看起來是依序處理。
回傳資料
在初期,我們不需要真的有商品資料,因此只需要有一個寫死的商品資料即可。
在開始實現功能之前,我們先根據需求描述我們要求的測試案例。增加新的 features/products.feature
來描述商品相關的行為。
1#language:zh-TW
2功能: 商品
3 場景: 回傳商品列表
4 當 我開啟 "/api/products"
5 那麼 我會看到 JSON 陣列包含 "name" 為 "Ruby 秘笈"
6 那麼 我會看到 JSON 陣列包含 "price" 為 100
在這個新的功能中,我們描述了一個 JSON 陣列裡面會包含「Ruby 秘笈」和價格 100 的資訊,大致上預期要看到這樣的回傳。
1[
2 { "name": "Ruby 秘笈", "price": 100 }
3]
因為是新的步驟,我們繼續在 features/step_definitions/common.rb
裡面進行擴充。
1Then('我會看到 JSON 陣列包含 {string} 為 {string}') do |key, value|
2 json = JSON.parse(last_response.body)
3
4 expect(json).to include(a_hash_including({ key => value }))
5end
6
7Then('我會看到 JSON 陣列包含 {string} 為 {int}') do |key, value|
8 json = JSON.parse(last_response.body)
9
10 expect(json).to include(a_hash_including({ key => value }))
11end
基本上只有差在數值一個是用 {string}
另一個是用 {int}
來對比,這是一種相對比較通用的定義方式,假設經常有這樣類型的情況出現,可以考慮直接定義 我會看到這些商品資料
這樣的描述,並且直接用 Table 功能一次性的填入所有預期看到的結果。
接下來我們只需要對 app.rb
快速地增加一個 /api/products
端點即可。
1# frozen_string_literal: true
2
3# ...
4
5module Shop
6 # :nodoc:
7 class API < Grape::API
8 format :json
9
10 # ...
11
12 namespace 'api' do
13 get '/products' do
14 [
15 { id: 1, name: 'Ruby 秘笈', price: 100 },
16 { id: 2, name: 'RSpec 秘笈', price: 150 }
17 ]
18 end
19 end
20 end
21end
接著運行 bundle exec cucumber
就可以看到能夠順利通過後端的驗證,我們還要去把前端部分修改。
前端整合
這次採取前後端分離的實作,我們需要對前端修改來指定 API 伺服器的位置。另一方面,因為伺服器是分離的,還需要對後端加上 CORS 的設定才能夠正確的被讀取。
在 Gemfile
裡面加入這行,增加 CORS 支援。
1# ...
2gem 'rack-cors'
運行 bundle install
安裝完畢後,我們需要修改 config.ru
加入 CORS 的設定,描述允許哪些域名存取。
1# frozen_string_literal: true
2
3require_relative 'app'
4
5use Rack::Cors do
6 allow do
7 origins '*'
8 resource '*', headers: :any, methods: %i[get post patch put]
9 end
10end
11
12run Shop::API
爲了方面測試,我們直接使用 *
來允許所有情況。
處理好後端後,用 bundle exec rackup
啟動伺服器,讓待會前端可以看到這個服務的內容。
在前端的實作中,我們已經把 API 呼叫集中到 src/api.ts
中,我們統一對所有 API 呼叫加上 ${API_SERVER}
的資訊來指定伺服器。
1const API_SERVER = import.meta.env.VITE_API_SERVER || ''
2
3export const fetchProducts = async () => await fetch(`${API_SERVER}/api/products`).then(res => res.json())
4export const checkout = async (items: Record<number, number>) => {
5 const res = await fetch(`${API_SERVER}/api/checkout`, {
6 method: "POST",
7 headers: {
8 "Content-Type": "application/json",
9 },
10 body: JSON.stringify(items)
11 })
12 const data = await res.json()
13
14 return [res.ok, data]
15}
這裡我們使用了 Vite 的環境變數功能,利用 import.meta.env
的機制,可以將 .env
中以 VITE_
開頭的環境變數注入到 JavaScript 裡面,這些環境變數是會被編譯到前端內容中的,因此要注意不要把資料庫密碼這類資訊放到裡面。
指定完伺服器位置後,增加 .env.development
或者 .env.development.local
(只對自己生效)加入以下設定。
1VITE_API_SERVER=http://localhost:9292
這樣我們在使用 yarn dev
啟動 Vite 的開發伺服器時,就可以看到原本無法順利顯示出來的商品資訊正確的根據後端的內容呈現。
最後,還需要運行 yarn run cucumber-js
來確認測試正常,因為我們在沒有指定 VITE_API_SERVER
的時候讓他保持空白,自然就會退回到 /api/products
這樣被 Playwright 模擬過的回傳,就不會影響到原本的測試環境。
假設未來要改為整合測試,也只需要補上 VITE_API_SERVER
的設定,以及更新步驟定義就能夠順利相容。
在專案初期採取前後端分離會耗費比較多的人力和資源在處理這類整合的問題,許多框架大多會提供組合好的解決方案,像是 Rails 的
js-bundling
或者 Laravel 預設整合好 Vue.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 的文件測試法