Skip to content

2026-09-19

/ 16 分鐘閱讀

/ Testing Fundamentals

Testing Library 入門:像螢幕報讀器一樣,用 getByRole 找元素、模擬使用者互動 ​

延續前一篇用原生 DOM API 測試按鈕的做法,這篇改用 Testing Library 這套工具,把同樣的測試寫得更貼近使用者實際看到、操作頁面的方式。

Testing Library:一套圍繞 DOM 查詢的輔助工具 ​

前一篇 vitest.config 裡其實已經悄悄帶到 Testing Library 的蹤跡。Testing Library 提供一整套操作 DOM 的輔助工具,用純粹的 document.querySelector 之類的原生 API 去找元素、模擬互動,其實是可以做到的,但寫起來瑣碎又容易出錯,Testing Library 的目的就是讓這件事變得更順手。

如果只是操作原生的 DOM 節點,用的是 @testing-library/dom;如果是 React、Svelte、Vue 元件,則各自有對應的套件。除了針對各框架多做的那一層整合之外,這幾套工具的核心概念幾乎一樣,這篇先從最基礎的 DOM 版本開始。

用畫面的角度找元素,而不是用 DOM 結構 ​

Testing Library 匯出一個 screen 物件,可以把它想成瀏覽器畫面本身:

javascript
import { screen } from '@testing-library/dom';

Testing Library 裡最有意思的設計,是它提供的查詢方法(selector),都是依照螢幕報讀器(screen reader)尋找元素的方式在運作,而不是像 jQuery 或 document.querySelector 那樣直接對照 DOM 結構。這代表寫測試時得用「找不找得到、找起來像不像使用者(包含依賴輔助工具的使用者)眼中看到的樣子」去思考,也連帶逼著標記寫得更符合無障礙標準,讓測試品質跟頁面的無障礙程度綁在一起。

把按鈕放上頁面,再用 getByRole 找出它 ​

先把前一篇的 createButton() 放進頁面:

javascript
document.body.replaceChildren(createButton());

用 replaceChildren() 而不是 appendChild(),是為了確保每次測試開始前,頁面上都只有這次測試自己建立的按鈕,不會因為前一個測試留下的節點,讓頁面上同時存在好幾個按鈕。

接著用 getByRole 依照角色跟名稱找出這個按鈕:

javascript
const button = screen.getByRole('button', { name: 'Click Me' });

這裡有個值得留意的地方:getByRole 如果在頁面上找不到符合條件的元素,會直接拋出錯誤。也就是說,光是這一行程式碼成功執行,本身就已經隱含驗證了「頁面上確實有一個文字是 Click Me 的按鈕」這件事,不一定要再另外寫一行斷言去確認它存在。

用 fireEvent 觸發事件,取代直接呼叫 .click() ​

前一篇直接呼叫 button.click() 模擬點擊,這篇改用 Testing Library 提供的 fireEvent,用更接近瀏覽器事件機制的方式去觸發:

javascript
import { fireEvent, screen } from '@testing-library/dom';

it('should change the text to "Clicked!" when clicked', () => {
    document.body.replaceChildren(createButton());

    const button = screen.getByRole('button', { name: 'Click Me' });

    fireEvent.click(button);

    expect(button.textContent).toBe('Clicked!');
});

fireEvent.click 做的事情,是真的組出一個點擊事件、發送到這個按鈕上,而不是單純呼叫 DOM 元素上現成的 click() 方法,行為上更貼近瀏覽器實際處理事件的方式。

每次測試後清乾淨,避免頁面上按鈕愈疊愈多 ​

如果每個測試都往 document.body 裡塞一個新按鈕,卻沒有清掉上一個測試留下的內容,很快就會在頁面上同時存在好幾個按鈕,讓查詢結果變得混亂。解法是在每個測試跑完後,把 document.body 清空:

javascript
afterEach(() => {
    document.body.innerHTML = '';
});

這樣就能確保每個測試開始時,頁面永遠是乾淨的狀態,不會被前一個測試留下的節點干擾。

user-event:更貼近真實使用者行為的模擬 ​

fireEvent 已經比直接呼叫 .click() 更接近瀏覽器事件機制,但 Testing Library 生態系裡還有另一個搭配使用的套件 user-event,模擬的層次又更進一步:它不只是丟出一個點擊事件,而是把使用者實際操作時會發生的一連串行為都模擬出來,例如滑鼠移入、按下、放開等等,盡量重現一個真人使用者互動時真正會觸發的完整過程:

javascript
import userEvent from '@testing-library/user-event';

it('should change the text to "Clicked!" when clicked', async () => {
    document.body.replaceChildren(createButton());

    const button = screen.getByRole('button', { name: 'Click Me' });

    await userEvent.click(button);

    expect(button.textContent).toBe('Clicked!');
});

因為 user-event 提供的方法都是非同步的,測試函式必須宣告成 async,並且對每一次呼叫加上 await。

呼應 Arrange、Act、Assert ​

整理過後,這個測試依然是同一套安排、執行、驗證的結構:把按鈕安排到頁面上、確認它確實以預期的角色掛載上去,是安排;用 user-event 模擬一次真實的點擊,是執行;驗證文字內容有沒有照預期改變,是驗證。跟前一篇用原生 API 寫的版本相比,這裡驗證的內容一樣,但整個查詢與互動的方式都更貼近使用者(包含依賴螢幕報讀器的使用者)實際體驗頁面的方式。

複習 ​

Testing Library 選取 DOM 元素的核心設計原則是什麼? ​

Testing Library 強迫開發者使用依照螢幕報讀器尋找元素的方式來選取元素,這樣的設計能促使開發者寫出更具無障礙性的標記

Testing Library 裡,有什麼函式可以用來找到一個按鈕元素? ​

可以用 screen.getByRole() 來找按鈕,通常會搭配特定的 name 或 role 參數一起使用

Testing Library 裡的 fireEvent 跟 user-event 有什麼差異? ​

fireEvent 模擬的是基本的事件,而 user-event 則更完整地模擬使用者實際的互動行為,例如滑鼠移動、點擊,以及更複雜的操作序列

Testing Library 的查詢方式鼓勵採用什麼樣的測試方法? ​

Testing Library 透過強迫使用依照螢幕報讀器尋找元素的選取方式(例如 getByRole),鼓勵以無障礙為優先的測試方法,確保測試與標記都寫得具有無障礙性

為什麼 Testing Library 建議使用以角色(role)為基礎的選取方式? ​

以角色為基礎的選取方式,能確保測試用跟螢幕報讀器相同的方式找到元素,藉此促進整體網頁的無障礙品質

小測驗 ​

Testing Library 在網頁開發裡的主要用途是什麼? 提供操作 DOM 的輔助工具,並促使開發者寫出具有無障礙性的標記
Testing Library 裡,哪個工具是專門用來對 DOM 元素觸發瀏覽器事件的? fireEvent 這個工具
有哪個套件能搭配 Testing Library,提供更進階的使用者互動模擬? user-event 這個套件
Testing Library 的選取器設計,鼓勵採用什麼樣的測試方法? 以無障礙為核心、依照螢幕報讀器的方式進行測試

此文章是 FrontendMasters 上的 Testing Fundamentals 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 15 篇 / 共 27 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash