昨天我們完成了 Roman Numeral Kata,現在進入框架特定測試的新階段!想像一個場景:專案上線前夕,PM 緊張地問:「UI 組件都測試過了嗎?」你自信地回答:「每個組件都有完整的測試覆蓋!」這就是今天要學習的 React Testing Library。
基礎階段 Kata 階段 框架特定測試
Days 1-10 Days 11-17 Days 18-27
✅ ✅ 📍 Day 18
[RTL 測試基礎] <- 今天在這
⬇️
下一階段:更多測試技巧
在 React 中,組件測試允許我們:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
// 建立 tests/day18/test_rtl_basics.tsx
import { describe, test, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
function HealthCheck() {
return <div>Status: OK</div>
}
test('makes successful render', () => {
render(<HealthCheck />)
expect(screen.getByText('Status: OK')).toBeInTheDocument()
})
// 更新 tests/day18/test_rtl_methods.tsx
test('supports different interactions', () => {
// Render 組件
const { getByText } = render(<Button>Click me</Button>)
expect(getByText('Click me')).toBeInTheDocument()
// 使用 screen 查詢
render(<Input placeholder="Enter text" />)
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
// 查詢角色
render(<button>Submit</button>)
expect(screen.getByRole('button')).toBeInTheDocument()
})
test('handles user events', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render(<button onClick={handleClick}>Click me</button>)
await user.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
test('validates component state changes', async () => {
const user = userEvent.setup()
render(<Counter />)
expect(screen.getByText('Count: 0')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'Increment' }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
// 建立 src/components/Day18/TaskList.tsx
import { useState } from 'react'
interface Task {
id: number
title: string
completed: boolean
}
export function TaskList() {
const [tasks, setTasks] = useState<Task[]>([])
const [input, setInput] = useState('')
const addTask = () => {
if (input.trim()) {
setTasks([...tasks, {
id: Date.now(),
title: input,
completed: false
}])
setInput('')
}
}
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add task"
/>
<button onClick={addTask}>Add</button>
<ul>
{tasks.map(task => (
<li key={task.id}>{task.title}</li>
))}
</ul>
</div>
)
}
// 建立 tests/day18/test_task_list.tsx
import { describe, test, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TaskList } from '../../src/components/Day18/TaskList'
test('lists all tasks', () => {
render(<TaskList />)
expect(screen.getByPlaceholderText('Add task')).toBeInTheDocument()
})
test('creates new task', async () => {
const user = userEvent.setup()
render(<TaskList />)
const input = screen.getByPlaceholderText('Add task')
const button = screen.getByRole('button', { name: 'Add' })
await user.type(input, 'Learn RTL Testing')
await user.click(button)
expect(screen.getByText('Learn RTL Testing')).toBeInTheDocument()
})
test('validates task creation input', async () => {
const user = userEvent.setup()
render(<TaskList />)
const button = screen.getByRole('button', { name: 'Add' })
await user.click(button)
const tasks = screen.queryAllByRole('listitem')
expect(tasks).toHaveLength(0)
})
test('manages multiple tasks', async () => {
const user = userEvent.setup()
render(<TaskList />)
const input = screen.getByPlaceholderText('Add task')
const button = screen.getByRole('button', { name: 'Add' })
// 添加多個任務
await user.type(input, 'Task 1')
await user.click(button)
await user.type(input, 'Task 2')
await user.click(button)
// 驗證任務存在且輸入已清空
expect(screen.getByText('Task 1')).toBeInTheDocument()
expect(screen.getByText('Task 2')).toBeInTheDocument()
expect((input as HTMLInputElement).value).toBe('')
})
test('handles async operations', async () => {
const user = userEvent.setup()
render(<AsyncComponent />)
await user.click(screen.getByRole('button'))
// 等待元素出現
const result = await screen.findByText('Success!')
expect(result).toBeInTheDocument()
})
test('validates error handling', () => {
render(<FormWithValidation />)
const submitButton = screen.getByRole('button', { name: 'Submit' })
fireEvent.click(submitButton)
expect(screen.getByRole('alert')).toHaveTextContent('Required field')
})
試著實作這些功能的測試:
src/components/TaskList.tsx
:import { useState } from 'react'
interface Task {
id: string
title: string
completed: boolean
}
export function TaskList() {
const [tasks, setTasks] = useState<Task[]>([])
const [newTask, setNewTask] = useState('')
const [error, setError] = useState('')
const handleAddTask = () => {
if (!newTask.trim()) {
setError('Required field')
return
}
const task: Task = {
id: Date.now().toString(),
title: newTask,
completed: false
}
setTasks([...tasks, task])
setNewTask('')
setError('')
}
return (
<div>
<h2>Task List</h2>
{error && <div role="alert">{error}</div>}
<div>
<input
type="text"
aria-label="New task"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Enter a task"
/>
<button onClick={handleAddTask}>Add</button>
</div>
<ul>
{tasks.map(task => (
<li key={task.id}>
<span>{task.title}</span>
</li>
))}
</ul>
</div>
)
}
今天我們學習了 React Testing Library 的基礎:
React Testing Library 是組件測試的基石,讓我們能從用戶角度測試組件行為,而不是實作細節。
明天我們將學習更多框架特定的測試技巧,包括如何測試更複雜的應用場景。準備好繼續深入探索了嗎?明天見!🚀