測驗:React 元件測試:render、screen 與 userEvent
共 5 題,點選答案後會立即顯示結果
1. 在 Testing Library 中,render() 函數的主要功能是什麼?
2. 當你想確認某個元素「不存在」於畫面上時,應該使用哪個查詢方法?
3. 為什麼推薦使用 userEvent 而不是 fireEvent 來模擬使用者操作?
4. 以下程式碼中,使用 userEvent 時需要注意什麼?
await userEvent.click(screen.getByRole(‘button’));
5. 當元素會在 API 回應後才非同步出現時,應該使用哪個查詢方法?
一句話說明
用 Testing Library 渲染元件、查詢元素、模擬使用者操作來測試 React 元件。
這篇文章會教你
讀完這篇,你會看懂:
render()怎麼把元件渲染到測試環境screen的各種查詢方法(getByRole、getByText 等)userEvent怎麼模擬點擊和輸入
30 秒範例
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
test('點擊按鈕後數字增加', async () => {
render(<Counter />);
await userEvent.click(screen.getByRole('button'));
expect(screen.getByText('1')).toBeInTheDocument();
});
Code language: JavaScript (javascript)這段代碼做了什麼:
render(<Counter />)– 把 Counter 元件渲染到虛擬 DOMscreen.getByRole('button')– 找到按鈕元素userEvent.click(...)– 模擬使用者點擊expect(...).toBeInTheDocument()– 確認畫面上有 ‘1’
render() 函數:渲染元件到虛擬 DOM
最小範例
import { render } from '@testing-library/react';
import { MyComponent } from './MyComponent';
render(<MyComponent />); // 把元件渲染到測試環境
Code language: JavaScript (javascript)一句話:render() 就是在測試裡「打開這個元件」。
帶 props 的渲染
// 元件定義
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
// 測試
render(<Greeting name="Alice" />); // 傳入 props
Code language: JavaScript (javascript)render 回傳什麼
const { container, rerender, unmount } = render(<MyComponent />);
container // 渲染結果的 DOM 容器
rerender // 重新渲染(測試 props 變化用)
unmount // 卸載元件(測試清理邏輯用)
Code language: JavaScript (javascript)| 回傳值 | 用途 |
|---|---|
container |
取得渲染的 DOM 容器 |
rerender |
用新 props 重新渲染 |
unmount |
卸載元件 |
screen API:查詢元素
一句話說明
screen 提供各種方法來「找到畫面上的元素」。
最常用的查詢方法
import { render, screen } from '@testing-library/react';
render(<MyComponent />);
// 用角色查詢(推薦)
screen.getByRole('button'); // 找按鈕
screen.getByRole('textbox'); // 找輸入框
screen.getByRole('heading'); // 找標題
// 用文字查詢
screen.getByText('Submit'); // 找包含 "Submit" 的元素
screen.getByText(/submit/i); // 不分大小寫
// 用 label 查詢(表單推薦)
screen.getByLabelText('Email'); // 找 label 是 "Email" 的輸入框
// 用 placeholder 查詢
screen.getByPlaceholderText('Enter your name');
// 用 test id 查詢(最後手段)
screen.getByTestId('custom-element'); // 找 data-testid="custom-element"
Code language: JavaScript (javascript)getBy vs queryBy vs findBy
這是最容易搞混的地方:
| 方法 | 找不到時 | 用途 |
|---|---|---|
getBy |
拋出錯誤 | 元素一定存在 |
queryBy |
回傳 null | 確認元素不存在 |
findBy |
等待後拋出錯誤 | 元素會非同步出現 |
// 元素一定存在
const button = screen.getByRole('button');
// 確認元素不存在
expect(screen.queryByText('Error')).not.toBeInTheDocument();
// 等待元素出現(API 回應後才顯示)
const message = await screen.findByText('Success');
Code language: JavaScript (javascript)查詢多個元素
// getBy 只能找到一個,找到多個會報錯
screen.getByRole('listitem'); // 如果有多個會錯誤
// 用 getAllBy 找多個
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(3);
Code language: JavaScript (javascript)翻譯對照表
| 你會看到 | 意思 |
|---|---|
screen.getByRole('button') |
找到(唯一的)按鈕 |
screen.getByText('Hello') |
找到包含 “Hello” 的元素 |
screen.queryByText('Error') |
找 “Error”,找不到就回傳 null |
screen.findByText('Loaded') |
等待並找到 “Loaded” |
screen.getAllByRole('listitem') |
找到所有 listitem |
userEvent:模擬使用者操作
一句話說明
userEvent 模擬真實的使用者行為(點擊、打字、選擇)。
為什麼用 userEvent 不用 fireEvent
// fireEvent:直接觸發事件
fireEvent.click(button);
// userEvent:模擬真實使用者行為
await userEvent.click(button);
Code language: JavaScript (javascript)| 差異 | fireEvent | userEvent |
|---|---|---|
| 行為模擬 | 只觸發單一事件 | 模擬完整流程 |
| focus | 不會 focus | 會先 focus |
| hover | 不會 hover | 會先 hover |
| 推薦度 | 特殊情況 | 日常使用 |
一句話:userEvent 更像真人操作,測試更可靠。
點擊操作
import userEvent from '@testing-library/user-event';
test('點擊按鈕', async () => {
render(<Counter />);
const button = screen.getByRole('button');
await userEvent.click(button); // 單擊
await userEvent.dblClick(button); // 雙擊
});
Code language: JavaScript (javascript)輸入文字
test('輸入文字', async () => {
render(<LoginForm />);
const input = screen.getByLabelText('Email');
// 輸入文字
await userEvent.type(input, '[email protected]');
// 清除後輸入
await userEvent.clear(input);
await userEvent.type(input, '[email protected]');
});
Code language: JavaScript (javascript)選擇下拉選單
test('選擇選項', async () => {
render(<CountrySelector />);
const select = screen.getByRole('combobox');
await userEvent.selectOptions(select, 'TW');
});
Code language: JavaScript (javascript)翻譯對照表
| 你會看到 | 意思 |
|---|---|
userEvent.click(el) |
點擊元素 |
userEvent.type(el, 'text') |
輸入文字 |
userEvent.clear(el) |
清除輸入框 |
userEvent.selectOptions(el, 'value') |
選擇下拉選項 |
實戰範例
範例 1:測試按鈕點擊
待測元件:
// Counter.tsx
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span data-testid="count">{count}</span>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
Code language: JavaScript (javascript)測試:
// Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
test('點擊按鈕增加計數', async () => {
// Arrange:渲染元件
render(<Counter />);
// Act:點擊按鈕
await userEvent.click(screen.getByRole('button'));
// Assert:確認計數增加
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
test('多次點擊累加', async () => {
render(<Counter />);
const button = screen.getByRole('button');
await userEvent.click(button);
await userEvent.click(button);
await userEvent.click(button);
expect(screen.getByTestId('count')).toHaveTextContent('3');
});
Code language: JavaScript (javascript)範例 2:測試表單輸入
待測元件:
// LoginForm.tsx
function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<label>
Email
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</label>
<label>
Password
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</label>
<button type="submit">Login</button>
</form>
);
}
Code language: JavaScript (javascript)測試:
// LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
test('填寫表單並送出', async () => {
// Mock 送出函式
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// 填寫表單
await userEvent.type(screen.getByLabelText('Email'), '[email protected]');
await userEvent.type(screen.getByLabelText('Password'), 'password123');
// 點擊送出
await userEvent.click(screen.getByRole('button', { name: 'Login' }));
// 確認呼叫
expect(handleSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123'
});
});
Code language: JavaScript (javascript)範例 3:測試 props 變化
// Greeting.tsx
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
Code language: JavaScript (javascript)測試:
test('props 變化時更新顯示', () => {
const { rerender } = render(<Greeting name="Alice" />);
expect(screen.getByRole('heading')).toHaveTextContent('Hello, Alice!');
// 用新 props 重新渲染
rerender(<Greeting name="Bob" />);
expect(screen.getByRole('heading')).toHaveTextContent('Hello, Bob!');
});
Code language: JavaScript (javascript)AI 最常這樣寫
模式 1:基本測試結構
test('描述測試目的', async () => {
// Arrange
render(<Component />);
// Act
await userEvent.click(screen.getByRole('button'));
// Assert
expect(screen.getByText('Result')).toBeInTheDocument();
});
Code language: JavaScript (javascript)模式 2:測試非同步行為
test('載入資料後顯示', async () => {
render(<UserList />);
// 等待非同步內容出現
expect(await screen.findByText('User 1')).toBeInTheDocument();
});
Code language: JavaScript (javascript)模式 3:測試元素不存在
test('錯誤訊息預設不顯示', () => {
render(<Form />);
// 用 queryBy 確認不存在
expect(screen.queryByText('Error')).not.toBeInTheDocument();
});
Code language: JavaScript (javascript)Vibe Coder 檢查點
看到 React 測試代碼時確認:
- [ ] 用
render()渲染元件了嗎? - [ ] 查詢方法選對了嗎?(getBy/queryBy/findBy)
- [ ] 使用者操作用
userEvent而不是fireEvent嗎? - [ ]
userEvent有加await嗎? - [ ] 非同步操作用
findBy或加await了嗎?
常見錯誤
錯誤 1:忘記 await
// 錯誤:userEvent 需要 await
userEvent.click(button);
// 正確
await userEvent.click(button);
Code language: JavaScript (javascript)錯誤 2:找多個元素用 getBy
// 錯誤:有多個 listitem 會報錯
screen.getByRole('listitem');
// 正確:用 getAllBy
screen.getAllByRole('listitem');
Code language: JavaScript (javascript)錯誤 3:確認不存在用 getBy
// 錯誤:找不到會報錯
expect(screen.getByText('Error')).not.toBeInTheDocument();
// 正確:用 queryBy
expect(screen.queryByText('Error')).not.toBeInTheDocument();
Code language: JavaScript (javascript)延伸:知道就好
這些進階功能遇到再查:
- within:在特定容器內查詢元素
- waitFor:等待某個條件成立
- waitForElementToBeRemoved:等待元素消失
- renderHook:測試自定義 Hook
總結
| 工具 | 用途 | 一句話 |
|---|---|---|
render() |
渲染元件 | 在測試裡「打開」元件 |
screen.getByRole() |
查詢元素 | 用角色找元素(推薦) |
screen.getByText() |
查詢元素 | 用文字找元素 |
screen.queryBy*() |
查詢元素 | 找不到回傳 null |
screen.findBy*() |
查詢元素 | 等待元素出現 |
userEvent.click() |
模擬點擊 | 像真人一樣點 |
userEvent.type() |
模擬輸入 | 像真人一樣打字 |
下一篇,我們會學習如何使用 Mock 來測試 API 呼叫和模組依賴。
進階測驗:React 元件測試:render、screen 與 userEvent
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在測試一個使用者清單元件,清單會顯示多個使用者項目。你想確認清單中有 5 個使用者項目,應該使用哪個查詢方法? 情境題
2. 同事寫了以下測試程式碼,測試執行時會報錯。問題出在哪裡? 錯誤診斷
test(‘確認錯誤訊息不存在’, () => {
render(<Form />);
expect(screen.getByText(‘Error’)).not.toBeInTheDocument();
});
3. 你在測試一個登入表單,需要模擬使用者輸入帳號密碼並點擊登入按鈕。以下哪個測試流程最符合最佳實踐? 情境題
4. 以下測試程式碼有時通過有時失敗(flaky test)。最可能的原因是什麼? 錯誤診斷
test(‘載入使用者資料’, async () => {
render(<UserProfile userId=”123″ />);
userEvent.click(screen.getByRole(‘button’, { name: ‘Load’ }));
expect(screen.getByText(‘John Doe’)).toBeInTheDocument();
});
5. 你正在測試一個 Greeting 元件,需要驗證當 props 從 “Alice” 變成 “Bob” 時,畫面會正確更新。應該使用 render() 回傳的哪個方法? 情境題
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}