# 3-1~3-3 Integration Testing
## 今日環境
- React
- jest
- React Testing Library
- user-event
## fireEvent vs. userEvent
fireEvent 是透過自己的 JaveScript「觸發事件」,而 userEvent 是 「模擬真實使用者操作」,如:使用者的手點滑鼠、打鍵盤、按 Tab 鍵、輸入文字,等完整互動行為。
<span style="color:red">實務上開發更偏向使用 userEvent</span>
| 比較項目 | `fireEvent` | `userEvent` |
| --- | --- | --- |
| 事件深度 | ⚙️ 低階 | 🧑 真實使用者模擬 |
| 模擬按鍵組合 | ❌ 不支援(你要自己 dispatch) | ✅ `userEvent.keyboard('{Enter}')` |
| 模擬輸入文字 | ❌ 不會觸發所有事件(input/change) | ✅ `userEvent.type(input, 'hello')` |
| 模擬滑鼠點擊 | ✅ `fireEvent.click(...)` | ✅ `userEvent.click(...)`(更完整) |
| 是否冒泡 / 阻止預設 | 手動處理 | 自動處理 |
| 是否推薦使用 | ⚠️ 備用 | ✅ 主流做法 |
| 是否 async | ❌ sync(非 async) | ✅ async(通常需要 await) |
## 3-1 整合測試(Integration Testing)
整合測試(Integration Testing,簡稱 I&T)又稱功能測試(Function Testing),是
針對多個程式碼片段或模組合併後的整體運作進行測試,比起 Unit Test 更接近真實場景。
它涵蓋:
- 元件與元件之間的合作行為
- 套件與 API 整合流程
- 從資料取得到畫面渲染的結果
### 圖例說明

### 實作範例
#### ImageList 元件
```typescript
import ImageItem from './ImageItem';
export default function ImageList({data,}: {
data: { id: number; title: string; image: string }[];
}) {
const renderImages = () => {
return data.map(({ id, title, image }) => {
return <ImageItem key={id} title={title} image={image} data-testid="image-item" />;
});
};
const renderNoDataPrompt = () => {
return <div data-testid="no-data-prompt">No data to display.</div>;
};
return <>{data.length ? renderImages() : renderNoDataPrompt()}</>;
}
```
#### ImageItem 元件
```tsx
export default function ImageItem({ title, image, ...props }: { title: string; image: string }) {
return (
<>
<div {...props}>
<img src={image} alt="" />
<p>{title}</p>
</div>
</>
);
}
```
#### 測試
🔹 顯示「無資料」
```typescript
it('沒有資料時,是否顯示No data', async () => {
const mockedData = [];
const { getByTestId } = render(<ImageList data={mockedData} />);
expect(getByTestId('no-data-prompt')).toHaveTextContent('No data to display.');
});
```
🔹 正常顯示圖片資料
```typescript
it('渲染正確資料長度', () => {
const mockedData = [
{ id: 1, title: 'image1.jpg', image: 'Image 1' },
{ id: 2, title: 'image2.jpg', image: 'Image 2' },
];
const { getAllByTestId } = render(<ImageList data={mockedData} />);
expect(getAllByTestId('image-item')).toHaveLength(2);
});
```
## 3-2 以使用者角度測試功能和盡量擬真
從「使用者操作」來驗證功能是否正確,盡量避免過度 mock,保持測試真實性。
- 從使用者的角度來測試功能
- 盡量擬真,盡量減少模擬取代真實狀況,不要使用過假資料的資料,並且盡量讓元件完全渲染
```typescript
describe('以使用者角度模擬', () => {
it('模擬使用者輸入*加法*', async () => {
const { getByTestId } = render(<Calculator />);
await userEvent.type(getByTestId('num-one'), '1');
await userEvent.type(getByTestId('num-two'), '2');
await userEvent.selectOptions(getByTestId('operator'), '+');
await userEvent.click(getByTestId('calculator-button'));
expect(getByTestId('result')).toHaveValue('3');
});
it('模擬使用者輸入*加法*', async () => {
const { getByTestId } = render(<Calculator />);
await userEvent.type(getByTestId('num-one'), '1');
await userEvent.type(getByTestId('num-two'), '2');
await userEvent.selectOptions(getByTestId('operator'), '-');
await userEvent.click(getByTestId('calculator-button'));
expect(getByTestId('result')).toHaveValue('-1');
});
it('模擬使用者輸入*乘法*', async () => {
const { getByTestId } = render(<Calculator />);
await userEvent.type(getByTestId('num-one'), '1');
await userEvent.type(getByTestId('num-two'), '2');
await userEvent.selectOptions(getByTestId('operator'), '*');
await userEvent.click(getByTestId('calculator-button'));
expect(getByTestId('result')).toHaveValue('2');
});
it('模擬使用者輸入*除法*', async () => {
const { getByTestId } = render(<Calculator />);
await userEvent.type(getByTestId('num-one'), '1');
await userEvent.type(getByTestId('num-two'), '2');
await userEvent.selectOptions(getByTestId('operator'), '/');
await userEvent.click(getByTestId('calculator-button'));
expect(getByTestId('result')).toHaveValue('0.5');
});
});
```
## 3-3 模擬元件、API、第三方函式庫
> 模擬(mock) 是讓測試更穩定、聚焦、可控的工具,常用於隔離依賴與減少副作用。
模擬用途:
- 隔絕依賴:不受外部狀態(API 回傳、DB 狀態等)影響測試結果
- 控制變因:精確控制 props / response 狀況進行驗證
- 減少副作用:避免真的發送網路請求、改資料、操作 DOM
### 模擬元件
- 聚焦測試父元件的行為
- 減少 render 時的複雜度與依賴
- 加快測試速度,避免不必要渲染
[jest.mock(moduleName, factory, options)](https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options)
- moduleName:字串,代表要被 mock 的模組路徑
- factory:回傳一個模擬模組的函式
```typescript
jest.mock('../src/components/ImageItem', () => () => <div>這是 ImageItem 元件</div>);
describe('ImageList', () => {
it('渲染正確資料長度', () => {
const mockedData = [
{ id: 1, title: 'Image 1', image: 'image1.jpg' },
{ id: 2, title: 'Image 2', image: 'image2.jpg' },
];
render(<ImageList data={mockedData} />);
screen.debug();
const items = screen.getAllByText('這是 ImageItem 元件');
expect(items).toHaveLength(2);
});
});
```

### 模擬第三方套件
- 減少外部依賴,不需要真的去發 HTTP 請求(快速又穩定)
```typescript
import { render, screen } from '@testing-library/react';
import ImageListData from '../../src/components/ImageListData';
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('ImageListData', () => {
it('應該正確渲染兩筆資料', async () => {
const mockData = {
data: {
data: [
{
id: 1,
email: 'aaa@test.com',
avatar: 'https://bit.ly/4228IT0',
first_name: 'John',
last_name: 'Doe',
},
{
id: 2,
email: 'bbb@test.com',
avatar: 'https://bit.ly/4b5nLP0',
first_name: 'Jane',
last_name: 'Doe',
},
],
},
};
mockedAxios.get.mockResolvedValue(mockData);
render(<ImageListData />);
const items = await screen.findAllByTestId('image-item');
expect(items).toHaveLength(2);
});
});
```
### 為何要盡量真實? 模擬不好嗎?
- 資料結構改變後,mock 沒同步更新 → 測試通過但功能壞了
- 第三方行為變動,mock 無法涵蓋新邏輯
- mock 元件視覺不一致,造成畫面異常未被察覺