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

結帳與結果 - Cucumber 的文件測試法

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

透過 Playwright 的 Mock API 機制,我們已經有了非常簡單的後端整合機制,接下來我們要繼續加上結帳的按鈕並且模擬成功跟失敗的兩種情境,讓我們在不依賴後端的狀況下完成一個非常基礎的購物車前端實現。

新增規格

在結帳的機制上,我們預期如果有商品就回傳「結帳成功」反之則會得到「結帳失敗」的訊息,因此在原本的 features/product.feature 文件中繼續增加新的描述。

 1#language:zh-TW
 2# ...
 3  場景: 蒼時購物車有物品時,可以進行結帳並且看到成功的結果
 4 把 "Ruby 秘笈" 加入購物車
 5    並且 進行結帳
 6    那麼 可以看見購物車總金額為 "$0"
 7    並且 可以看見購物車商品數為 0
 8    並且 可以看見結帳結果為 "結帳成功"
 9
10
11  場景: 蒼時購物車沒有物品時,可以進行結帳並且看到失敗的結果
12 進行結帳
13    那麼 可以看見購物車總金額為 "$0"
14    並且 可以看見購物車商品數為 0
15    並且 可以看見結帳結果為 "結帳失敗"

這兩條描述呈現了成功跟失敗的情況外,也要求送出後會將購物車清空的行為,因此我們需要再實作時也將這點考慮進去。

因為增加了兩個新的步驟,繼續擴充 features/step_definitions/common.ts 來讓 Cucumber 知道該如何處理這兩種情況。

1// ...
2When('進行結帳', async function(this: World) {
3  await this.page.getByRole('button', { name: '結帳' }).click()
4})
5
6// ...
7Then('可以看見結帳結果為 {string}', async function(this: World, result: string) {
8  await expect(this.page.getByText(result)).toBeVisible()
9})

跟前面的行為相比,因為相對的單純,我們可以直接尋找「結帳」的按鈕點選,以及確認頁面上是否存在「結帳成功」或者「結帳失敗」來做驗證。

如果頁面非常複雜,明確的指定或者縮小範圍還是會讓測試跑得更快一些,尤其時這些測試都有一個超時的設定,單一步驟處理過久不是一件好事。

Mock API

跟商品資料不同的地方在於,結帳行為理論上要由後端處理,並且不應該有預期外的行為產生。因此我們不會利用假設(Given)的方式定義,因此直接在 feature/support/world.ts 中進行擴充,讓這個路徑可以被呼叫。

 1// ...
 2
 3Before(async function() {
 4  await this.page.route('/api/checkout', async route => {
 5    const cartItems = route.request().postDataJSON()
 6    const json = { text: '結帳成功' }
 7    const isValid = Object.values(cartItems).length > 0
 8
 9    if(!isValid) {
10      json.text = '結帳失敗'
11    }
12
13    await route.fulfill({
14      status: isValid ? 200 : 422,
15      json
16    })
17  })
18})

當我們有實際的後端可以使用時,可以選擇把這段邏輯拆除。在這裡我們從請求中取得 JSON 物件,並且根據傳入的購物車數量決定要回傳成功還是失敗的訊息。

在 Playwright API 中,可以呼叫 route.fetch() 來實際發出請求,並且利用 route.fulfill() 來改寫回傳內容,除了直接模擬行為外,也可以善用改寫機制來輔助測試。

實作結帳

最後我們將結帳的實作加入到 src/App.vue 裡面來完成這個功能,先在原本的購物車區段加入新的按鈕與結果的區塊。

1<template>
2  <div class="cart">
3    <div v-if="hadCheckout">{{ checkoutResult }}</div>
4    <div data-testid="cart-amount">一共 {{ totalItems }} 項商品</div>
5    <div data-testid="cart-subtotal">總金額為 ${{ subtotal }}</div>
6    <button @click="onCheckout">結帳</button>
7  </div>
8  <!--more-->
9</template>

這次增加了 onCheckout 方法來送出購物車,以及 hadCheckoutcheckoutResult 來對應結帳後的結果,繼續修改 TypeScript 的部分加入對應的實作。

 1<script setup lang="ts">
 2// ...
 3
 4const checkoutResult = ref(null)
 5const onCheckout = () => fetch('/api/checkout', {
 6  method: "POST",
 7  headers: {
 8    "Content-Type": "application/json",
 9  },
10  body: JSON.stringify(cart)
11})
12  .then(res => {
13    if(res.ok) {
14      for(let id in cart) {
15        delete cart[id]
16      }
17    }
18
19    return res
20  })
21  .then(res => res.json())
22  .then(data => checkoutResult.value = data.text)
23const hadCheckout = computed(() => checkoutResult.value !== null)
24</script>

這裡的結果會需要觸發 Vue 重新繪製畫面,因此我們需要用 ref(null) 來初始化,並且用 computed() 來檢查是否為 null 如果已經有 API 的結果回傳,我們就可以將內容顯示出來。

onCheckout 方法的實作上,我們直接使用 fetch() 來對 /api/checkout 端點進行呼叫,並且在回傳結果為成功(Response.ok 表示狀態碼在 200 ~ 299 之間)的時候對購物車進行清空的處理。

因為 reactive({}) 只對修改物件生效,因此我們無法用 cart = {} 這類方式修改,只能呼叫迴圈將所有被紀錄的商品找出,用 delete cart[id] 的方式清除掉記憶體的資訊。

最後,回傳是以文字訊息的方式呈現,因此直接將結果放到 checkoutResult 中顯示出來即可。

到此為止,一個非常簡單的購物功能就完成,我們需要對現有的實作進行一些調整來支援跟後端的整合處理。