---
title: "商品資料 API - Cucumber 的文件測試法"
date: 2024-03-29T00:00:00+08:00
publishDate: 2024-03-29T00:00:00+08:00
lastmod: 2023-12-06T15:50:15+08:00
tags: ["Cucumber","教學","測試","後端","Ruby","Grape"]
series: "test-with-cucumber"
toc: true
permalink: "https://blog.aotoki.me/posts/2024/03/29/test-with-cucumber-product-api/"
language: "zh-tw"
---


之前因為使用 Playwright 的方式造假後端 API 造成前端的實際畫面是無法使用，接下來在後端的部分我們要將商品 API 完成一個雛形讓前端可以恢復正常。

> 在實際的開發流程中，前後端確認完畢 API 的資料結構後會同步進行，我們切分成兩個段落因此看起來是依序處理。

<!--more-->

## 回傳資料{#return-product-data}

在初期，我們不需要真的有商品資料，因此只需要有一個寫死的商品資料即可。

在開始實現功能之前，我們先根據需求描述我們要求的測試案例。增加新的 `features/products.feature` 來描述商品相關的行為。

```gherkin
#language:zh-TW
功能: 商品
  場景: 回傳商品列表
    當 我開啟 "/api/products"
    那麼 我會看到 JSON 陣列包含 "name" 為 "Ruby 秘笈"
    那麼 我會看到 JSON 陣列包含 "price" 為 100
```

在這個新的功能中，我們描述了一個 JSON 陣列裡面會包含「Ruby 秘笈」和價格 100 的資訊，大致上預期要看到這樣的回傳。

```json
[
  { "name": "Ruby 秘笈", "price": 100 }
]
```

因為是新的步驟，我們繼續在 `features/step_definitions/common.rb` 裡面進行擴充。

```ruby
Then('我會看到 JSON 陣列包含 {string} 為 {string}') do |key, value|
  json = JSON.parse(last_response.body)

  expect(json).to include(a_hash_including({ key => value }))
end

Then('我會看到 JSON 陣列包含 {string} 為 {int}') do |key, value|
  json = JSON.parse(last_response.body)

  expect(json).to include(a_hash_including({ key => value }))
end
```

基本上只有差在數值一個是用 `{string}` 另一個是用 `{int}` 來對比，這是一種相對比較通用的定義方式，假設經常有這樣類型的情況出現，可以考慮直接定義 `我會看到這些商品資料` 這樣的描述，並且直接用 Table 功能一次性的填入所有預期看到的結果。

接下來我們只需要對 `app.rb` 快速地增加一個 `/api/products` 端點即可。

```ruby
# frozen_string_literal: true

# ...

module Shop
  # :nodoc:
  class API < Grape::API
    format :json

    # ...

    namespace 'api' do
      get '/products' do
        [
          { id: 1, name: 'Ruby 秘笈', price: 100 },
          { id: 2, name: 'RSpec 秘笈', price: 150 }
        ]
      end
    end
  end
end
```

接著運行 `bundle exec cucumber` 就可以看到能夠順利通過後端的驗證，我們還要去把前端部分修改。

## 前端整合{#frontend-integration}

這次採取前後端分離的實作，我們需要對前端修改來指定 API 伺服器的位置。另一方面，因為伺服器是分離的，還需要對後端加上 CORS 的設定才能夠正確的被讀取。

在 `Gemfile` 裡面加入這行，增加 CORS 支援。

```ruby
# ...
gem 'rack-cors'
```

運行 `bundle install` 安裝完畢後，我們需要修改 `config.ru` 加入 CORS 的設定，描述允許哪些域名存取。

```ruby
# frozen_string_literal: true

require_relative 'app'

use Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: %i[get post patch put]
  end
end

run Shop::API
```

爲了方面測試，我們直接使用 `*` 來允許所有情況。

處理好後端後，用 `bundle exec rackup` 啟動伺服器，讓待會前端可以看到這個服務的內容。

在前端的實作中，我們已經把 API 呼叫集中到 `src/api.ts` 中，我們統一對所有 API 呼叫加上 `${API_SERVER}` 的資訊來指定伺服器。

```ts
const API_SERVER = import.meta.env.VITE_API_SERVER || ''

export const fetchProducts = async () => await fetch(`${API_SERVER}/api/products`).then(res => res.json())
export const checkout = async (items: Record<number, number>) => {
  const res = await fetch(`${API_SERVER}/api/checkout`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(items)
  })
  const data = await res.json()

  return [res.ok, data]
}
```

這裡我們使用了 Vite 的環境變數功能，利用 `import.meta.env` 的機制，可以將 `.env` 中以 `VITE_` 開頭的環境變數注入到 JavaScript 裡面，這些環境變數是會被編譯到前端內容中的，因此要注意不要把資料庫密碼這類資訊放到裡面。

指定完伺服器位置後，增加 `.env.development` 或者 `.env.development.local`（只對自己生效）加入以下設定。

```bash
VITE_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 等等，會比分開處理更容易。

