---
# System prepended metadata

title: 3-1~3-3 Integration Testing

---

# 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 整合流程
- 從資料取得到畫面渲染的結果

### 圖例說明
![image](https://hackmd.io/_uploads/SyXRwnPkxg.png)


### 實作範例
#### 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);
  });
});
```

![image](https://hackmd.io/_uploads/Hk6xdnP1lg.png)

### 模擬第三方套件
- 減少外部依賴，不需要真的去發 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 元件視覺不一致，造成畫面異常未被察覺