某天,我們從設計師那裡拿到了設計圖
於是我們開始切版,跟往常一樣,主要注重在畫面的樣式與互動上,此時並不會考慮到 E2E 測試
若開發期間,很幸運地拿到了 QA 的測試案例(或任何寫測試案例的人),我們是否可以在切版的當下,也將未來寫測試案例的情況也加入考慮?
此外,這裡提到的測試案例是指 E2E 測試(自動化測試),而非人工的整合測試
為了讓寫測試的過程能更順利,依據測試案例上的各種 action,例如:點擊某個 button、在 input 輸入文字,或是從某個 div 元素上獲取文字。在這些計畫做事的 DOM 元件上安插屬性(attribute)作為 querySelector
的查詢指標
若手上沒有測試案例又該如何呢?沒有測試案例,就無法準確知道哪些元素會被查詢,只能用推測的。此時腦中閃過一個想法:
如果制定一些規則,然後按照這個規則去添加屬性,是否就可以維持一定的覆蓋率?
因此我根據自己寫測試的經驗,嘗試制定了一些規則。我將會分成以下幾個部分說明:
- 元件取向
- 位置取向
- 狀態取向
- 無形資料取向
- 結構取向
以下所有程式,我會以 React 元件來解釋。實際上無論框架,最終都會回到 HTML 元素身上
元件取向
頁面元件(Page Component)
插入 data-test-page
屬性於最外層的 HTML 元素:data-test-page='<頁面元件名稱>'
有了 data-test-page
屬性,我們可以很快地辨認出這個頁面元件的範圍(我更喜歡稱之為「邊界」)
註:屬性名稱全看個人喜好,這裡展示的是我自己覺得不錯的命名方式 🙃
例如我們有一個 home 元件:
function HomePage() {
return <div data-test-page="home">{/* ... */}</div>;
}
基本元件(Basic Component)
插入 data-test-comp
屬性於最外層的 HTML 元素:data-test-comp='<元件名稱>'
有了 data-test-comp
屬性,我們可以很快地辨認出這個基本元件的範圍
function Button() {
return <div data-test-comp="button">{/* ... */}</div>;
}
位置取向
元件會在不同地方重複使用(這也是我們要抽成元件的初衷,DRY 原則),因此,data-test-comp
就會出現重複的情況,而且如果在相近的地方有多個重複的元件,就不太好快速辨識目標元件
比如說我們在一個頁面上,用了 5 個 button 元件,而測試案例的動作是:「選取其中一個 button,然後點擊它。」此時,我們必須先找出所有 button 的元素,然後在裡面尋找我們要做點擊的那個 button(比對 button 的文字;或是直接指定第幾個 button 元素 ⋯⋯ 等)
這個過程不困難,但我認為這個過程很煩,而且不斷重複在做這個查找的動作。於是我開始思考,是否有更精確、更快速的方式。如果有一個唯一值(或接近唯一的值,亦即很少重複),這個元素尋找的過程將會簡單許多
因此,像是這樣的情境,我們需要另一個表示功能的特徵1 屬性(這個特徵屬性最好是唯一值)
使用 data-test-feat
屬性,用來辨識這個元件在這裡所提供的功能(或目的)
我認為這個屬性比較偏向是非強制性的。倘若只有在元件上標示屬性,仍然可以拿到我們想要的元件。例如:在導覽列(navbar 元件)上的登入 button 及選單裡(menu 元件)的登入 button,分別包含在不同元件裡面。此外,過度使用可能會造成最後很多重複的屬性,唯一性的特徵消失了,精準打擊(aka 準確查詢)的初衷也會隨之化為泡影。因此,要在這之間取得平衡
所以,data-test-feat
屬性大概有兩種加入方式:
在容器元素(container element)
有時候,基於元件的樣式設計(有外層決定寬高、背景色 ⋯⋯ 等),我們會在元件的外面包一層容器元素
function HomePage() {
return (
<>
{/* ... */}
<div data-test-feat="confirmBtn">
<Button />
</div>
<div data-test-feat="cancelBtn">
<Button />
</div>
{/* ... */}
</>
);
}
上面的範例,可以很清楚地知道,在 home 頁面裡,有兩個 button:確認 button 及取消 button
如果在元件外面包一層容器元素不是你的菜,可以透過 props
的方式穿進去元件裡:
透過 props 安插在元件上
若沒有外層的容器元素,透過 props 傳進去是另一個選擇:
function HomePage() {
return (
<>
{/* ... */}
<Button dataTestFeat="confirmBtn" />
<Button dataTestFeat="cancelBtn" />
{/* ... */}
</>
);
}
在 button 元件裡:
function Button({ dataTestFeat, btnText, labelText }) {
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-feat={dataTestFeat}>{btnText}</button>
</label>
</div>
);
}
狀態取向
有時候我們想知道某個元件的狀態,假如我們有一個 toggle button 元件:
測試案例中,我們想要在點擊 toggle button 之後,確認它會確實從 on 狀態轉變成 off 的狀態
我們當然可以透過辨認 style 的變化得知它的狀態。如果是用 SASS 的話,或許可以很簡單地從 class 上辨認出(端看 class 怎麼設計了)。然而我最近比較常用的是 tailwindcss,此時就會變得不太直覺。以下是我的 toggle button 元件:
function ToggleButton() {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<button
type="button"
className="rounded-full overflow-hidden relative w-16 h-7 shrink-0 text-white font-semibold text-sm uppercase ml-2.5 bg-neutral-500"
onClick={handleChange}
>
{/* here is what the difference by state */}
<div className={`absolute transition left-1.5 top-1/2 -translate-y-1/2 ${isOn && "translate-x-9"}`}>
<div className="rounded-full bg-white w-4 h-4 relative">
<div className="absolute right-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">On</div>
<div className="absolute left-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">Off</div>
</div>
</div>
</button>
);
}
這個元件裡,translate-x-9
是 on 與 off 狀態上差異的 class。我們仍然可以辨識,但會造成修改樣式時後容易抓不到元素的情況。Utility first 樣式庫的優點,在此時卻成了缺點
像這種情況,建議加入表示狀態的屬性:
function Button({ btnText, labelText }) {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-state={isOn} onClick={handleChange}>
{btnText}
</button>
</label>
</div>
);
}
另一個常見的情境是載入狀態(loading),假如有一個列表與搜尋列元件:
function SearchPage() {
// ...some state here
return (
<div data-test-page="searchPage">
<div data-test-comp="searchBar">
<input value={searchValue} data-test-feat="searchInput" />
<button onClick={handleSearch} data-test-feat="searchBtn">
Search
</button>
</div>
<div data-test-comp="list" data-test-loading={isLoading}>
{/* list data here */}
</div>
</div>
);
}
註:基於可讀性,我直接用 HTML 表示
點擊搜尋 button 之後,頁面就會開始 loading 打 API 拿資料,並在結束後,結束 loading 狀態,並更新資料至底下的列表
無形資料取向
假如有一個 card 元件要顯示商品資訊,如在元素上存在 product id 的話,就能快速地找到目標商品。然而商品資訊卡片通常不會顯示 product id,此時就必須將這個值加到屬性當中:
function ProductCard({ productId, ...restProps }) {
return (
<div data-test-comp="productCard" data-test-product-id={productId}>
{/* product card content */}
</div>
);
}
至於命名原則,我認為只要夠語意化就行了
結構取向
模擬元素(Simulated Element)
在一些特殊情境下,我們會需要用 div
元素去模擬其他元素
這裡舉個例子,用 div
元素去模擬 table
元素的結構。加入 data-test-el
屬性:data-test-el='<模擬的元素名稱>'
function Table() {
return (
<div data-test-el="table">
<div data-test-el="tbody">{/* ... */}</div>
</div>
);
}
或許會有人不太懂為何要這樣做,但我還是可以簡單說明一下
在開發的時候,遇到某些樣式在與 table 有關的元素上(也就是<table>
、<thead>
、<tbody>
⋯⋯ 等),在 Safari 瀏覽器下的顯示會不如預期(Safari 的 bug ⋯⋯),因此必須用 div
元素重建整個 table 的結構,因此在加入屬性之後,可以很快地了解整個 table 的結構,藉此增加可讀性
元件的結構語意化
誠如模擬元素,為了增加可讀性,我們也可以在其他地方加入屬性,可以更快地了解整個元件結構。例如一個 dialog 元件:
function Dialog() {
return (
<div data-test-comp="dialog">
<div data-test-el="dialogHeader">this is dialog header</div>
<div data-test-el="dialogBody">this is dialog body</div>
<div data-test-el="dialogFooter">this is dialog footer</div>
</div>
);
}
加入 data-test-el
屬性之後,可以有效率地一眼看出整個元件的結構(在這裡,dialog 分為 dialog header、dialog body、dialog footer 三個區塊)
結語
開發時,加入屬性將會是一個漫長的過程。在開發的過程中,測試案例並非我們的優先考量。制定規則並遵守它,可以讓我們無腦地加入屬性,並將專注力放在實作樣式外觀、操作互動以及資料上。當開發結束後,撰寫測試案例將會更為順利,並感謝以前的自己
最後,說到底這些規則畢竟只是自己的想法,並非什麼業界標準那麼偉大的東西 😎
開發愉快
Feature,我翻譯成「特徵」 ↩︎