博客 / 詳情

返回

Jest與React Testing Library:前端測試的最佳實踐

Jest 和 React Testing Library (RTL) 是前端開發中用於測試 React 應用的首選工具。Jest 是一個功能豐富的JavaScript測試框架,而React Testing Library 是一種提倡以用户角度編寫測試的庫,它鼓勵測試組件的行為而不是內部實現細節。

安裝和配置

首先,確保你已經安裝了react, react-dom, jest, @testing-library/react, 和 @testing-library/jest-dom。在你的package.json中添加以下依賴:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom
# 或
yarn add --dev jest @testing-library/react @testing-library/jest-dom

jest.config.js中配置Jest,例如:

module.exports = {
  setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
  testEnvironment: 'jsdom',
};

基本測試結構

創建一個測試文件,通常與你的組件文件同名,但帶有.test.js.test.tsx後綴。下面是一個簡單的組件測試示例:

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import MyComponent from './MyComponent';

describe('MyComponent', () => {
  it('renders correctly', () => {
    render(<MyComponent />);
    expect(screen.getByText('Hello, world!')).toBeInTheDocument();
  });

  it('handles button click', () => {
    render(<MyComponent />);
    const button = screen.getByRole('button', { name: /click me/i });
    fireEvent.click(button);
    expect(screen.getByText(/clicked/i)).toBeInTheDocument();
  });
});

測試組件行為

使用render函數渲染組件,並使用screen對象來查詢DOM,確保組件按預期渲染。getByText, getByRole, getByPlaceholderText等輔助函數可以幫助找到元素。

模擬(Mocking)

Jest 提供了強大的模擬功能,可以模擬組件的依賴,例如API調用。例如,模擬一個fetch調用:

import fetch from 'jest-fetch-mock';

beforeAll(() => {
  fetch.mockResponseOnce(JSON.stringify({ data: 'mocked response' }));
});

it('fetches data on mount', async () => {
  render(<MyComponent />);
  await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
});

事件處理

使用fireEvent函數觸發組件上的事件,比如點擊按鈕或提交表單。

const button = screen.getByRole('button');
fireEvent.click(button);

清理和解構

在每個測試之後,確保清理掉任何副作用,如添加到DOM中的元素。afterEach鈎子可以用於此目的:

afterEach(() => {
  cleanup();
});

異步測試

使用waitForasync/await處理異步操作,確保組件在測試中達到期望狀態:

it('loads data after fetching', async () => {
  render(<MyComponent />);
  await waitFor(() => expect(screen.getByText('Data loaded')).toBeInTheDocument());
});

測試狀態和副作用

使用jest.useFakeTimers()act函數來測試狀態變化和副作用,如定時器或副作用函數:

jest.useFakeTimers();

it('displays loading state', () => {
  render(<MyComponent />);
  act(() => jest.advanceTimersByTime(1000));
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

組件庫的測試

對於複雜的組件庫,可以創建一個setupTests.js文件來設置全局的模擬和配置,例如:

import '@testing-library/jest-dom';
import fetchMock from 'jest-fetch-mock';

fetchMock.enableMocks(); // 如果使用fetch模擬

性能優化

使用jest-environment-jsdom-sixteenjest-environment-jsdom-thirteen可以減少測試的內存消耗。

測試組件的交互性

React Testing Library 強調測試組件的行為,而不是它的實現細節。以下是一些測試組件交互性的最佳實踐:

測試用户交互

使用fireEvent模擬用户行為,例如點擊、輸入和選擇:

const input = screen.getByLabelText('Search');
fireEvent.change(input, { target: { value: 'search term' } });
expect(input).toHaveValue('search term');

確保組件響應變化

測試組件如何響應狀態或props的變化:

const toggleButton = screen.getByRole('button', { name: 'Toggle' });
fireEvent.click(toggleButton);
expect(screen.getByTestId('visible-element')).toBeInTheDocument();

驗證數據渲染

測試組件是否正確呈現從API獲取的數據:

const data = { title: 'Example Title' };
fetchMock.mockResponseOnce(JSON.stringify(data));

render(<MyComponent />);
await waitFor(() => expect(screen.getByText('Example Title')).toBeInTheDocument());

錯誤和異常處理

測試組件在錯誤發生時的行為,例如驗證錯誤消息的顯示:

it('displays error message when fetching fails', async () => {
  fetchMock.mockRejectOnce(new Error('Network error'));
  render(<MyComponent />);
  await waitFor(() => expect(screen.getByText('Error: Network error')).toBeInTheDocument());
});

清晰的測試描述

編寫有意義的測試描述,讓測試結果易於理解:

it('renders search results when query is submitted', async () => {
  // ...
});

測試組件的邊緣情況

確保覆蓋組件的所有邊緣情況,包括空值、異常數據和邊界條件:

it('displays loading state when data is fetching', () => {
  render(<MyComponent isLoading />);
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('displays empty state when no data is found', () => {
  render(<MyComponent data={[]} />);
  expect(screen.getByText('No results found.')).toBeInTheDocument();
});

代碼覆蓋率報告

使用jest-coverage插件生成代碼覆蓋率報告,確保有足夠的測試覆蓋:

npx jest --coverage

持續集成

將測試集成到持續集成(CI)流程中,確保代碼質量始終如一:

# .github/workflows/test.yml (GitHub Actions)
name: Test

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '14.x'
    - name: Install dependencies
      run: npm ci
    - name: Run tests
      run: npm test

高級測試技巧

Mocking和Spying

Jest 提供了模擬(mocking)和監聽(spying)功能,用於控制和檢查函數行為:

import myFunction from './myFunction';

jest.spyOn(myModule, 'myFunction');

// 在測試中調用函數
myFunction();

// 檢查函數是否被調用
expect(myFunction).toHaveBeenCalled();

// 檢查函數調用的具體參數
expect(myFunction).toHaveBeenCalledWith(expectedArgs);

// 重置模擬
myFunction.mockReset();

// 重置並清除模擬的返回值和調用記錄
myFunction.mockClear();

// 恢復原函數
myFunction.mockRestore();

測試異步邏輯

使用async/awaitawait waitFor處理異步操作:

it('fetches data and updates state', async () => {
  // 模擬API返回
  fetchMock.mockResolvedValueOnce({ json: () => Promise.resolve({ data: 'mocked data' }) });

  render(<MyComponent />);

  // 等待數據加載完成
  await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));

  // 驗證狀態更新
  expect(screen.getByText('mocked data')).toBeInTheDocument();
});

測試生命週期方法

使用act包裹組件的生命週期方法,確保它們在測試環境中正確執行:

import { act } from 'react-dom/test-utils';

it('calls componentDidMount', () => {
  const mockFn = jest.fn();
  const MyComponent = () => {
    useEffect(mockFn);
    return <div>Component</div>;
  };

  act(() => {
    render(<MyComponent />);
  });

  expect(mockFn).toHaveBeenCalled();
});

使用createRef和forwardRef

測試使用createRefforwardRef的組件時,可以創建一個ref並傳遞給組件:

it('sets focus on the input element', () => {
  const inputRef = React.createRef();
  render(<MyComponent inputRef={inputRef} />);

  act(() => {
    inputRef.current.focus();
  });

  expect(document.activeElement).toBe(inputRef.current);
});

測試事件處理器

使用fireEvent模擬事件,但要確保在act中進行:

it('calls onChange handler', () => {
  const onChangeHandler = jest.fn();
  render(<MyComponent onChange={onChangeHandler} />);

  const input = screen.getByRole('textbox');
  act(() => {
    fireEvent.change(input, { target: { value: 'new value' } });
  });

  expect(onChangeHandler).toHaveBeenCalledWith('new value');
});

性能優化

快速測試

減少渲染深度:只渲染必要的組件層級,避免渲染整個組件樹。
使用jest.spyOn代替jest.fn:對於性能敏感的函數,使用jest.spyOn代替jest.fn,因為它更快。

選擇性運行測試

使用--findRelatedTests選項只運行與更改相關的測試,以加快測試速度:

npx jest --findRelatedTests

使用快照測試

對於不經常更改的組件,使用快照測試可以節省時間:

it('renders snapshot correctly', () => {
  const { container } = render(<MyComponent />);
  expect(container.firstChild).toMatchSnapshot();
});

代碼覆蓋率閾值

設置代碼覆蓋率閾值,確保測試覆蓋了足夠的代碼:

// jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 80,
      functions: 80,
      lines: 80,
    },
  },
};

2500G計算機入門到高級架構師開發資料超級大禮包免費送!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.