---
title: "商品資料與總價 - Cucumber 的文件測試法"
date: 2024-03-01T00:00:00+08:00
publishDate: 2024-03-01T00:00:00+08:00
lastmod: 2023-12-06T15:50:15+08:00
tags: ["Cucumber","教學","測試","前端","Vite","Vue","Playwright"]
series: "test-with-cucumber"
toc: true
permalink: "https://blog.aotoki.me/posts/2024/03/01/test-with-cucumber-product-list-and-subtotal/"
language: "zh-tw"
---


現階段我們已經具備了新增跟移除商品的機制，然而要跟後端搭配的話就無法避免跟真實的 API 來進行串接，在測試環境中就不會那麼好處理。我們可以利用 Playwright 的 Mock API 機制來模擬我們想要的 API 回應。

<!--more-->

## 商品資料{#product-data}

如果要驗證實作出來的前端正確無誤，我們會需要提供不同的測試資料來進行驗證。然而，我們是透過啟動一個 Vite 伺服器來進行處理，因此無法直接在 Cucumber 的實作中修改 Vue 放在記憶體中的變數。

那麼，為了實現這件事情，就需要讓我們可以透過呼叫 API 的方式來載入這些資料。首先，先對原本的 Cucumber 測試做出調整，加入描述測試資料的部分。

```gherkin
#language:zh-TW
功能: 商品列表
  背景:
    假設 這裡有一些商品
      | id | name       | price |
      | 1  | Ruby 秘笈  | 100   |
      | 2  | RSpec 秘笈 | 150   |
    當 開啟網站

# ...
```

在前面的例子中，我們都是直接寫死在 `src/App.vue` 中來通過測試。因為 Cucumber 是以一個功能（Feature）為單位來描述，我們應該嘗試將完整的狀況在裡面描述，那麼商品資料也最好能像這樣反應出來。

接下來，我們要加入「這裡有一些商品」這個步驟應有的反應。

```ts
Given('這裡有一些商品', async function(this: World, data: DataTable) {
  await this.page.route('/api/products', async route => {
    const json = data.hashes().map(item => ({
      id: Number(item.id),
      price: Number(item.price),
      name: item.name
    }))

    await route.fulfill({ json })
  })
})
```

在這段程式碼中，利用 Playwright 的 [Mock API](https://playwright.dev/docs/mock) 機制，讓我們呼叫 `/api/products` 這個路徑時，會直接回應我們設定的內容，而不是實際上進行這個呼叫。

因為 Cucumber 的 `DataTable` 物件剛好可以回傳一個我們預期的資料結構，可以直接用 `{ json: data.hashes()` 作為傳回值，讓我們在 Vue 端可以拿到像這樣的內容。

```json
[
  { "id": 1, "name": "Ruby 秘笈", "price": 100 },
  { "id": 2, "name": "RSpec 秘笈", "price": 150 },
]
```

## 總價計算{#calculate-amount}

現在我們得到了一個帶有更多資訊的商品資料，接下來要在原本計算商品數量的前提下，再繼續擴充出「總價計算」的情境，因此還需要一個對應的測試案例來反應這件事情。

```gherkin
#language:zh-TW
# ...
  場景: 蒼時可以對商品點選加入購物車，並看到總金額發生變化
    當 把 "<name>" 加入購物車
    那麼 可以看見購物車總金額為 "<subtotal>"
    例子:
      | name       | subtotal |
      | Ruby 秘笈  | $100     |
      | RSpec 秘笈 | $150     |
```

為了驗證金額會確實的產生變化，這次我們透過舉例的方式，來反應不同商品會得到不一樣的金額。接下來要增加新的步驟實作，並且稍微調整原本的 `cart` 為 `cart-amount` 來對應不同購物車屬性的元素。

```ts
Then('可以看見購物車商品數為 {int}', async function(this: World, amount: number) {
  await expect(this.page.getByTestId('cart-amount')).toHaveText(`一共 ${amount} 項商品`)
})

Then('可以看見購物車總金額為 {string}', async function(this: World, subtotal: string) {
  await expect(this.page.getByTestId('cart-subtotal')).toHaveText(`總金額為 ${subtotal}`)
})
```

有了新的定義後，再繼續修改 `src/App.vue` 調整我們的 HTML 結構來將購物車組合成一個群組，顯示不同的資訊。

```vue
<template>
  <div data-testid="cart">
    <div data-testid="cart-amount">一共 {{ totalItems }} 項商品</div>
    <div data-testid="cart-subtotal">總金額為 ${{ subtotal }}</div>
  </div>
  <!-- 略 -->
</template>
```

因為增加了 `{{ subtotal }}` 的呼叫，我們還需要調整 TypeScript 的實作，同時要將透過 API 載入商品資料的部分實作進來。

```vue
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'

const products = ref([])
const findByProductId = id => products.value.find(product => product.id === Number(id))

fetch('/api/products')
  .then(res => res.json())
  .then(data => products.value = data)

// ...

const subtotal = computed(() => Object.entries(cart).reduce((prev, [id, count]) => prev + findByProductId(id)?.price * count, 0))
</script>
```

首先，我們讓 `products` 變數成為一個 Vue 的參考，並且實作了 `findByProductId()` 方法來作為後續 `subtotal` 計算的輔助，並且用 `fetch` 來載入商品資料，最後用跟 `totalItems` 相同方式，以 `computed()` 的實作來製作一個可以反映變化的實作。

現階段直接用瀏覽器預覽會發生錯誤，我們還沒有真實的後端 API 可用，然而運行 `yarn run cucumber-js` 是可以順利通過測試，因為我們利用 Playwright 來幫助我們模擬後端。

> 這篇文章的例子是作為示範用只有單一檔案，因此直接呼叫 `fetch()` 來取的商品資料，在正式專案或者比較完整的實作中，還是推薦封裝成 API Client 來使用。

