---
title: "商品列表與加入購物車 - Cucumber 的文件測試法"
date: 2024-02-16T00:00:00+08:00
publishDate: 2024-02-16T00: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/02/16/test-with-cucumber-products-and-add-to-cart/"
language: "zh-tw"
---


我們現在已經準備好了可以使用 Cucumber 撰寫功能測試（Feature Test）的開發環境，接下來我們會用前端實作購物車的功能並且測試，然後接續實現後端完成一個具備非常基礎功能的前後端分離專案。

<!--more-->

## 商品列表{#product-list}

一個購物車的基本要素，不外乎就是商品列表、購物車的管理這兩項功能，因此我們會先實現一個可以列出商品列表的機制，來讓我們可以實現「加入購物車」的功能。

> 使用 Vite 建立的專案會有一些預設的範例，在開始之前可以清除掉沒有使用到的檔案，以及我們驗證環境正常的 `features/hello.feature` 這類檔案。

首先，我們加入 `features/products.feature` 這個檔案，作為商品列表的測試檔案。

```gherkin
#language:zh-TW
功能: 商品列表
  場景: 蒼時可以看到 Ruby 秘笈和 RSpec 秘笈兩本書
	當 開啟網站
    那麼 可以看見商品 "Ruby 秘笈"
    並且 可以看見商品 "RSpec 秘笈"
```

我們的實作很簡單，只需要將兩項商品呈現出來就達到目的。

> 為了方便理解，這邊會使用 `language:zh-TW` 的標記讓 Cucumber 以中文方式撰寫。

因為「開啟網站」和「看間商品」這兩個步驟我們還沒有定義過，因此需要修改 `features/step_definitions/common.ts` 加入新的定義。

```ts
import { When, Then } from "@cucumber/cucumber"
import { expect } from "@playwright/test"

import World from '../support/world'

When('開啟網站', async function() {
  await this.visit('/')
})

Then('可以看見商品 {string}', async function (this: World, targetText: string) {
  await expect(this.page.getByText(targetText)).toBeVisible()
});
```

基本上 Cucumber 這類測試會希望模擬「真實操作」狀況，因此大多會從首頁開始，同時 Vue 在我們的使用情境下也是 SPA（Single Page Application，單頁式應用）作為前提，因此只需要很簡單的實現 `await this.visit('/')` 來開啟前端畫面即可。

至於看見商品的實作，我們利用 Playwright 的定位器（Locator）機制，直接尋找我們希望查詢的文字（如：Ruby 秘笈）並且預期 `toBeVisible()`（可見的）就能驗證我們可以在畫面上看見這個商品。

要通過這個測試也不困難，我們直接替換掉 `src/App.vue` 這個檔案的內容，直接寫上內容即可。

```vue
<template>
  <div>
    <div>Ruby 秘笈</div>
    <div>RSpec 秘笈</div>
  </div>
</template>

<script setup lang="ts">
</script>
```

試著運行 `yarn run cucumber-js` 會發現測試順利通過，在這個階段我們不需要去擔心資料的來源、畫面呈現或者最終的呈現效果，因為只有一個測試是無法完整的涵蓋這個功能。

## 加入購物車{#add-to-cart}

在這一個階段，我們會希望可以有一個「加入購物車」的按鈕，並且在點選後改變計數的呈現，因此我們可以繼續增加測試。

```gherkin
#language:zh-TW
功能: 商品列表
  # ...
  場景: 蒼時可以對 Ruby 秘笈點選加入購物車，並看到商品數為 1
	當 開啟網站
    並且 把 "Ruby 秘笈" 加入購物車
    那麼 可以看見購物車商品數為 1
```

看起來並不複雜，我們跟呈現商品列表一樣先將測試的步驟實現出來。

```ts
import { When, Then } from "@cucumber/cucumber"
import { expect } from "@playwright/test"

import World from '../support/world'

// ...

When('把 {string} 加入購物車', async function(this: World, productName: string) {
  const product = await this.page.getByTestId(/product-/).filter({ hasText: productName })
  await product.getByRole('button', { name: '加入購物車' }).click()
})

// ...

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

相對前面的步驟，這一次相對的複雜不少。首先我們需要可以識別「不同商品品項」在 Playwright 的 Locator 選項上，使用 CSS 選取並不算方便，這裡評估後認為最適合的是 `testid`（測試識別）的模式，因此我們預期所有的商品都會有 `product-1` 這樣的 ID 被標記，那麼條件就是具備 `product-` 開頭並且包含了商品名稱的元素。

接下來找到這個元素中「加入購物車」的按鈕去點選他。

商品數量的斷言條件就相對容易，我們一樣用 `testid` 去找到具備 `cart` 名稱的元素，然後檢查內容具備 `一共 ? 項商品` 這樣的文字。

繼續更新 `src/App.vue` 的內容，來通過這個測試的要求。

```vue
<template>
  <div data-testid="cart">一共 1 項商品</div>
  <div>
    <div data-testid="product-1">
      Ruby 秘笈
      <button>加入購物車</button>
    </div>
    <div data-testid="product-2">
      RSpec 秘笈
      <button>加入購物車</button>
    </div>
  </div>
</template>

<script setup lang="ts">
</script>
```

也許你會感到疑惑，沒有實作任何「邏輯」是沒問題的嗎？這是因為我們的測試描述的情境還不夠完善，因此會出現像這樣的狀況，這段實作確實能通過目前的測試的。

## 商品計數{#product-counter}

很明顯的，前面一個步驟的實作是無法達到「統計購物車數量」的期待，我們可以透過增加新的測試條件來完善這個情境。

```gherkin
#language:zh-TW
功能: 商品列表
  背景:
    當 開啟網站

  # ...

  場景: 蒼時可以對 Ruby 秘笈點選加入購物車，並看到商品數為 1
    並且 把 "Ruby 秘笈" 加入購物車
    那麼 可以看見購物車商品數為 1

  場景: 蒼時可以對 Ruby 秘笈和 RSpec 秘笈點選加入購物車，並看到商品數為 2
    當 把 "Ruby 秘笈" 加入購物車
    並且 把 "RSpec 秘笈" 加入購物車
    那麼 可以看見購物車商品數為 2
```

因為「開啟網站」是所有場景都共用的，我們可以抽取出來變成背景資訊的描述，這裡我們增加了新的商品數量描述，來反應「商品數量變化」的要求。

接下來我們就可以修正 `src/App.vue` 的實作，讓購物車的商品計數可以反應出這樣的狀況。

```vue
<template>
  <div data-testid="cart">一共 {{ cart.count }} 項商品</div>
  <div>
    <div data-testid="product-1">
      Ruby 秘笈
      <button @click="onAddToCart">加入購物車</button>
    </div>
    <div data-testid="product-2">
      RSpec 秘笈
      <button @click="onAddToCart">加入購物車</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive } from 'vue'


const cart = reactive({ count: 0 })
const onAddToCart = () => cart.count++
</script>
```

這個版本的實作，我們使用 Vue 3 的 `reactive()` 紀錄了一個物件的變化，裡面呈現了 `cart` 的計數，並且讓每個「加入購物車」按鈕都對應到 `onAddToCart()` 這個方法，用來對購物車的數量進行遞增，那麼就可以順利的呈現出數量變化的需求。

> 為了讓測試容易實現，我們盡量控制每一個階段的實作，讓實作變得簡單可控以及盡量跟著 Cucumber 所撰寫的文件拓展，逐步的完善，這樣也能更快地發現一些沒注意到的盲點。

