蒼時弦也
蒼時弦也
資深軟體工程師
發表於

商品資料與總價 - Cucumber 的文件測試法

這篇文章是 Cucumber 的文件測試法 系列的一部分。

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

商品資料

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

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

 1#language:zh-TW
 2功能: 商品列表
 3  背景:
 4    假設 這裡有一些商品
 5      | id | name       | price |
 6      | 1  | Ruby 秘笈  | 100   |
 7      | 2  | RSpec 秘笈 | 150   |
 8     開啟網站
 9
10# ...

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

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

 1Given('這裡有一些商品', async function(this: World, data: DataTable) {
 2  await this.page.route('/api/products', async route => {
 3    const json = data.hashes().map(item => ({
 4      id: Number(item.id),
 5      price: Number(item.price),
 6      name: item.name
 7    }))
 8
 9    await route.fulfill({ json })
10  })
11})

在這段程式碼中,利用 Playwright 的 Mock API 機制,讓我們呼叫 /api/products 這個路徑時,會直接回應我們設定的內容,而不是實際上進行這個呼叫。

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

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

總價計算

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

1#language:zh-TW
2# ...
3  場景: 蒼時可以對商品點選加入購物車,並看到總金額發生變化
4 把 "<name>" 加入購物車
5    那麼 可以看見購物車總金額為 "<subtotal>"
6    例子:
7      | name       | subtotal |
8      | Ruby 秘笈  | $100     |
9      | RSpec 秘笈 | $150     |

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

1Then('可以看見購物車商品數為 {int}', async function(this: World, amount: number) {
2  await expect(this.page.getByTestId('cart-amount')).toHaveText(`一共 ${amount} 項商品`)
3})
4
5Then('可以看見購物車總金額為 {string}', async function(this: World, subtotal: string) {
6  await expect(this.page.getByTestId('cart-subtotal')).toHaveText(`總金額為 ${subtotal}`)
7})

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

1<template>
2  <div data-testid="cart">
3    <div data-testid="cart-amount">一共 {{ totalItems }} 項商品</div>
4    <div data-testid="cart-subtotal">總金額為 ${{ subtotal }}</div>
5  </div>
6  <!--  -->
7</template>

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

 1<script setup lang="ts">
 2import { ref, reactive, computed } from 'vue'
 3
 4const products = ref([])
 5const findByProductId = id => products.value.find(product => product.id === Number(id))
 6
 7fetch('/api/products')
 8  .then(res => res.json())
 9  .then(data => products.value = data)
10
11// ...
12
13const subtotal = computed(() => Object.entries(cart).reduce((prev, [id, count]) => prev + findByProductId(id)?.price * count, 0))
14</script>

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

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

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