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

商品列表與加入購物車 - Cucumber 的文件測試法

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

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

商品列表

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

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

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

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

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

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

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

 1import { When, Then } from "@cucumber/cucumber"
 2import { expect } from "@playwright/test"
 3
 4import World from '../support/world'
 5
 6When('開啟網站', async function() {
 7  await this.visit('/')
 8})
 9
10Then('可以看見商品 {string}', async function (this: World, targetText: string) {
11  await expect(this.page.getByText(targetText)).toBeVisible()
12});

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

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

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

1<template>
2  <div>
3    <div>Ruby 秘笈</div>
4    <div>RSpec 秘笈</div>
5  </div>
6</template>
7
8<script setup lang="ts">
9</script>

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

加入購物車

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

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

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

 1import { When, Then } from "@cucumber/cucumber"
 2import { expect } from "@playwright/test"
 3
 4import World from '../support/world'
 5
 6// ...
 7
 8When('把 {string} 加入購物車', async function(this: World, productName: string) {
 9  const product = await this.page.getByTestId(/product-/).filter({ hasText: productName })
10  await product.getByRole('button', { name: '加入購物車' }).click()
11})
12
13// ...
14
15Then('可以看見購物車商品數為 {int}', async function(this: World, amount: number) {
16  await expect(this.page.getByTestId('cart')).toHaveText(`一共 ${amount} 項商品`)
17})

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

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

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

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

 1<template>
 2  <div data-testid="cart">一共 1 項商品</div>
 3  <div>
 4    <div data-testid="product-1">
 5      Ruby 秘笈
 6      <button>加入購物車</button>
 7    </div>
 8    <div data-testid="product-2">
 9      RSpec 秘笈
10      <button>加入購物車</button>
11    </div>
12  </div>
13</template>
14
15<script setup lang="ts">
16</script>

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

商品計數

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

 1#language:zh-TW
 2功能: 商品列表
 3  背景:
 4 開啟網站
 5
 6  # ...
 7
 8  場景: 蒼時可以對 Ruby 秘笈點選加入購物車,並看到商品數為 1
 9    並且 把 "Ruby 秘笈" 加入購物車
10    那麼 可以看見購物車商品數為 1
11
12  場景: 蒼時可以對 Ruby 秘笈和 RSpec 秘笈點選加入購物車,並看到商品數為 2
13 把 "Ruby 秘笈" 加入購物車
14    並且 把 "RSpec 秘笈" 加入購物車
15    那麼 可以看見購物車商品數為 2

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

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

 1<template>
 2  <div data-testid="cart">一共 {{ cart.count }} 項商品</div>
 3  <div>
 4    <div data-testid="product-1">
 5      Ruby 秘笈
 6      <button @click="onAddToCart">加入購物車</button>
 7    </div>
 8    <div data-testid="product-2">
 9      RSpec 秘笈
10      <button @click="onAddToCart">加入購物車</button>
11    </div>
12  </div>
13</template>
14
15<script setup lang="ts">
16import { reactive } from 'vue'
17
18
19const cart = reactive({ count: 0 })
20const onAddToCart = () => cart.count++
21</script>

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

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