【Vitest React 測試教學】#02 React 元件測試:render、screen 與 userEvent

測驗:React 元件測試:render、screen 與 userEvent

共 5 題,點選答案後會立即顯示結果

1. 在 Testing Library 中,render() 函數的主要功能是什麼?

  • A. 將元件部署到正式伺服器
  • B. 將元件渲染到測試環境的虛擬 DOM
  • C. 編譯 TypeScript 程式碼
  • D. 自動產生測試報告

2. 當你想確認某個元素「不存在」於畫面上時,應該使用哪個查詢方法?

  • A. getByText()
  • B. findByText()
  • C. queryByText()
  • D. getAllByText()

3. 為什麼推薦使用 userEvent 而不是 fireEvent 來模擬使用者操作?

  • A. userEvent 執行速度比較快
  • B. userEvent 模擬完整的使用者行為流程(如 focus、hover),更接近真人操作
  • C. fireEvent 已被官方棄用
  • D. userEvent 不需要安裝額外套件

4. 以下程式碼中,使用 userEvent 時需要注意什麼?

await userEvent.click(screen.getByRole(‘button’));
  • A. userEvent 的操作是非同步的,必須加上 await
  • B. 必須先呼叫 userEvent.setup() 初始化
  • C. click 方法只能用於 button 元素
  • D. 不能與 screen.getByRole 一起使用

5. 當元素會在 API 回應後才非同步出現時,應該使用哪個查詢方法?

  • A. getByText()
  • B. findByText()
  • C. queryByText()
  • D. getByTestId()

一句話說明

用 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)

這段代碼做了什麼

  1. render(<Counter />) – 把 Counter 元件渲染到虛擬 DOM
  2. screen.getByRole('button') – 找到按鈕元素
  3. userEvent.click(...) – 模擬使用者點擊
  4. 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 題,包含情境題與錯誤診斷題。

1. 你正在測試一個使用者清單元件,清單會顯示多個使用者項目。你想確認清單中有 5 個使用者項目,應該使用哪個查詢方法? 情境題

  • A. screen.getByRole('listitem')
  • B. screen.queryByRole('listitem')
  • C. screen.getAllByRole('listitem')
  • D. screen.findByRole('listitem')

2. 同事寫了以下測試程式碼,測試執行時會報錯。問題出在哪裡? 錯誤診斷

test(‘確認錯誤訊息不存在’, () => { render(<Form />); expect(screen.getByText(‘Error’)).not.toBeInTheDocument(); });
  • A. not.toBeInTheDocument() 語法錯誤
  • B. 應使用 queryByText 而非 getByText,因為 getByText 找不到時會直接拋出錯誤
  • C. 需要加上 await 關鍵字
  • D. render 函數使用方式錯誤

3. 你在測試一個登入表單,需要模擬使用者輸入帳號密碼並點擊登入按鈕。以下哪個測試流程最符合最佳實踐? 情境題

  • A. 使用 fireEvent.change 設定 input value,再用 fireEvent.click 點擊按鈕
  • B. 直接修改 DOM 元素的 value 屬性,再觸發 submit 事件
  • C. 使用 userEvent.type 輸入文字,再用 userEvent.click 點擊按鈕,並加上 await
  • D. 使用 screen.getByLabelText 找到 input 後直接設定 innerHTML

4. 以下測試程式碼有時通過有時失敗(flaky test)。最可能的原因是什麼? 錯誤診斷

test(‘載入使用者資料’, async () => { render(<UserProfile userId=”123″ />); userEvent.click(screen.getByRole(‘button’, { name: ‘Load’ })); expect(screen.getByText(‘John Doe’)).toBeInTheDocument(); });
  • A. userEvent.click 缺少 await,導致斷言可能在點擊完成前執行
  • B. getByRole 應該改用 queryByRole
  • C. render 需要加上 await
  • D. 測試函數不應該是 async

5. 你正在測試一個 Greeting 元件,需要驗證當 props 從 “Alice” 變成 “Bob” 時,畫面會正確更新。應該使用 render() 回傳的哪個方法? 情境題

function Greeting({ name }: { name: string }) { return <h1>Hello, {name}!</h1>; }
  • A. unmount() 卸載後重新 render()
  • B. rerender(<Greeting name="Bob" />)
  • C. container.innerHTML = '...' 直接修改 DOM
  • D. 呼叫 screen.refresh() 刷新畫面

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *