如何对使用 axios 的代码进行单元测试和 Mock?请说明常用的测试方法
对使用 axios 的代码进行测试时,需要掌握单元测试、集成测试和 Mock 技术。1. 使用 Jest 和 axios-mock-adapter安装依赖npm install --save-dev jest axios-mock-adapter @testing-library/react基础 Mock 测试// api/user.jsimport axios from 'axios';export const fetchUser = async (userId) => { const response = await axios.get(`/api/users/${userId}`); return response.data;};export const createUser = async (userData) => { const response = await axios.post('/api/users', userData); return response.data;};// __tests__/user.test.jsimport axios from 'axios';import MockAdapter from 'axios-mock-adapter';import { fetchUser, createUser } from '../api/user';describe('User API', () => { let mock; beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); }); test('fetchUser should return user data', async () => { const userData = { id: 1, name: 'John', email: 'john@example.com' }; mock.onGet('/api/users/1').reply(200, userData); const result = await fetchUser(1); expect(result).toEqual(userData); }); test('fetchUser should handle error', async () => { mock.onGet('/api/users/999').reply(404, { message: 'User not found' }); await expect(fetchUser(999)).rejects.toThrow(); }); test('createUser should create new user', async () => { const newUser = { name: 'Jane', email: 'jane@example.com' }; const createdUser = { id: 2, ...newUser }; mock.onPost('/api/users').reply(201, createdUser); const result = await createUser(newUser); expect(result).toEqual(createdUser); });});高级 Mock 配置// __tests__/api.test.jsimport axios from 'axios';import MockAdapter from 'axios-mock-adapter';describe('API Testing', () => { let mock; beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); }); test('mock network error', async () => { mock.onGet('/api/data').networkError(); await expect(axios.get('/api/data')).rejects.toThrow('Network Error'); }); test('mock timeout', async () => { mock.onGet('/api/data').timeout(); await expect(axios.get('/api/data')).rejects.toThrow('timeout'); }); test('mock with function', async () => { mock.onPost('/api/users').reply((config) => { const data = JSON.parse(config.data); if (!data.email) { return [400, { error: 'Email is required' }]; } return [201, { id: 1, ...data }]; }); // 测试成功 const response = await axios.post('/api/users', { name: 'John', email: 'john@example.com' }); expect(response.status).toBe(201); // 测试失败 await expect( axios.post('/api/users', { name: 'John' }) ).rejects.toThrow(); }); test('mock with headers', async () => { mock.onGet('/api/protected', { headers: { Authorization: 'Bearer token123' } }).reply(200, { data: 'protected' }); const response = await axios.get('/api/protected', { headers: { Authorization: 'Bearer token123' } }); expect(response.data).toEqual({ data: 'protected' }); }); test('mock with query params', async () => { mock.onGet('/api/search', { params: { q: 'test' } }) .reply(200, { results: [] }); const response = await axios.get('/api/search', { params: { q: 'test' } }); expect(response.data).toEqual({ results: [] }); });});2. 使用 MSW (Mock Service Worker)安装和配置npm install --save-dev msw// mocks/handlers.jsimport { rest } from 'msw';export const handlers = [ // GET 请求 rest.get('/api/users', (req, res, ctx) => { return res( ctx.status(200), ctx.json([ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ]) ); }), // GET 单个用户 rest.get('/api/users/:id', (req, res, ctx) => { const { id } = req.params; if (id === '999') { return res( ctx.status(404), ctx.json({ message: 'User not found' }) ); } return res( ctx.status(200), ctx.json({ id: Number(id), name: 'John' }) ); }), // POST 请求 rest.post('/api/users', async (req, res, ctx) => { const body = await req.json(); return res( ctx.status(201), ctx.json({ id: 3, ...body }) ); }), // PUT 请求 rest.put('/api/users/:id', async (req, res, ctx) => { const { id } = req.params; const body = await req.json(); return res( ctx.status(200), ctx.json({ id: Number(id), ...body }) ); }), // DELETE 请求 rest.delete('/api/users/:id', (req, res, ctx) => { return res(ctx.status(204)); })];// mocks/server.jsimport { setupServer } from 'msw/node';import { handlers } from './handlers';export const server = setupServer(...handlers);// jest.setup.jsimport { server } from './mocks/server';beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());使用 MSW 进行测试// __tests__/user.integration.test.jsimport { fetchUser, createUser, updateUser, deleteUser } from '../api/user';describe('User API Integration Tests', () => { test('should fetch all users', async () => { const users = await fetchUsers(); expect(users).toHaveLength(2); expect(users[0]).toHaveProperty('id'); expect(users[0]).toHaveProperty('name'); }); test('should fetch single user', async () => { const user = await fetchUser(1); expect(user).toEqual({ id: 1, name: 'John' }); }); test('should handle 404 error', async () => { await expect(fetchUser(999)).rejects.toThrow(); }); test('should create user', async () => { const newUser = { name: 'Bob', email: 'bob@example.com' }; const created = await createUser(newUser); expect(created).toMatchObject(newUser); expect(created).toHaveProperty('id'); }); test('should update user', async () => { const updates = { name: 'John Updated' }; const updated = await updateUser(1, updates); expect(updated.name).toBe('John Updated'); }); test('should delete user', async () => { await expect(deleteUser(1)).resolves.not.toThrow(); });});动态覆盖 Handler// __tests__/dynamic-mock.test.jsimport { rest } from 'msw';import { server } from '../mocks/server';test('should handle server error', async () => { // 临时覆盖 handler server.use( rest.get('/api/users', (req, res, ctx) => { return res( ctx.status(500), ctx.json({ error: 'Internal Server Error' }) ); }) ); await expect(fetchUsers()).rejects.toThrow();});test('should handle network error', async () => { server.use( rest.get('/api/users', (req, res) => { return res.networkError('Failed to connect'); }) ); await expect(fetchUsers()).rejects.toThrow('Failed to connect');});3. React 组件测试使用 React Testing Library// components/UserProfile.jsximport React, { useEffect, useState } from 'react';import axios from 'axios';export const UserProfile = ({ userId }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchUser = async () => { try { setLoading(true); const response = await axios.get(`/api/users/${userId}`); setUser(response.data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchUser(); }, [userId]); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );};// __tests__/UserProfile.test.jsximport React from 'react';import { render, screen, waitFor } from '@testing-library/react';import { UserProfile } from '../components/UserProfile';import { server } from '../mocks/server';import { rest } from 'msw';describe('UserProfile Component', () => { test('should display loading state initially', () => { render(<UserProfile userId={1} />); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); test('should display user data after loading', async () => { render(<UserProfile userId={1} />); await waitFor(() => { expect(screen.getByText('John')).toBeInTheDocument(); }); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); test('should display error message on failure', async () => { server.use( rest.get('/api/users/999', (req, res, ctx) => { return res(ctx.status(404)); }) ); render(<UserProfile userId={999} />); await waitFor(() => { expect(screen.getByText(/Error:/)).toBeInTheDocument(); }); });});4. Vue 组件测试使用 Vue Test Utils// components/UserProfile.vue<template> <div> <div v-if="loading">Loading...</div> <div v-else-if="error">Error: {{ error }}</div> <div v-else> <h1>{{ user.name }}</h1> <p>{{ user.email }}</p> </div> </div></template><script>import { ref, onMounted } from 'vue';import axios from 'axios';export default { props: ['userId'], setup(props) { const user = ref(null); const loading = ref(true); const error = ref(null); onMounted(async () => { try { const response = await axios.get(`/api/users/${props.userId}`); user.value = response.data; } catch (err) { error.value = err.message; } finally { loading.value = false; } }); return { user, loading, error }; }};</script>// __tests__/UserProfile.spec.jsimport { mount } from '@vue/test-utils';import { describe, it, expect } from 'vitest';import UserProfile from '../components/UserProfile.vue';import { server } from '../mocks/server';describe('UserProfile', () => { it('should display loading state initially', () => { const wrapper = mount(UserProfile, { props: { userId: 1 } }); expect(wrapper.text()).toContain('Loading...'); }); it('should display user data after loading', async () => { const wrapper = mount(UserProfile, { props: { userId: 1 } }); await wrapper.vm.$nextTick(); await new Promise(resolve => setTimeout(resolve, 0)); expect(wrapper.text()).toContain('John'); });});5. 自定义 Hook/Composable 测试// hooks/useApi.jsimport { useState, useEffect } from 'react';import axios from 'axios';export const useApi = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { setLoading(true); const response = await axios.get(url); setData(response.data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error };};// __tests__/useApi.test.jsimport { renderHook, waitFor } from '@testing-library/react';import { useApi } from '../hooks/useApi';describe('useApi Hook', () => { test('should return loading state initially', () => { const { result } = renderHook(() => useApi('/api/data')); expect(result.current.loading).toBe(true); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); }); test('should return data after successful request', async () => { const { result } = renderHook(() => useApi('/api/users')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toHaveLength(2); expect(result.current.error).toBeNull(); }); test('should return error on failed request', async () => { const { result } = renderHook(() => useApi('/api/error')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.error).not.toBeNull(); expect(result.current.data).toBeNull(); });});6. E2E 测试使用 Cypress// cypress/integration/api.spec.jsdescribe('API Tests', () => { beforeEach(() => { // 拦截 API 请求 cy.intercept('GET', '/api/users', { statusCode: 200, body: [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ] }).as('getUsers'); }); it('should display users list', () => { cy.visit('/users'); cy.wait('@getUsers'); cy.get('[data-testid="user-list"]').should('have.length', 2); cy.contains('John').should('be.visible'); }); it('should handle API error', () => { cy.intercept('GET', '/api/users', { statusCode: 500, body: { error: 'Server Error' } }).as('getUsersError'); cy.visit('/users'); cy.wait('@getUsersError'); cy.contains('Error loading users').should('be.visible'); }); it('should create new user', () => { cy.intercept('POST', '/api/users', { statusCode: 201, body: { id: 3, name: 'New User' } }).as('createUser'); cy.visit('/users/new'); cy.get('input[name="name"]').type('New User'); cy.get('button[type="submit"]').click(); cy.wait('@createUser').its('request.body').should('deep.equal', { name: 'New User' }); cy.url().should('include', '/users'); });});7. 测试最佳实践测试文件组织src/├── api/│ ├── user.js│ └── __tests__/│ └── user.test.js├── components/│ ├── UserProfile.jsx│ └── __tests__/│ └── UserProfile.test.jsx├── hooks/│ ├── useApi.js│ └── __tests__/│ └── useApi.test.js└── mocks/ ├── handlers.js └── server.js测试工具函数// test-utils.jsimport { render } from '@testing-library/react';import { QueryClient, QueryClientProvider } from '@tanstack/react-query';export const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, }, },});export function renderWithClient(ui) { const testQueryClient = createTestQueryClient(); const { rerender, ...result } = render( <QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider> ); return { ...result, rerender: (rerenderUi) => rerender( <QueryClientProvider client={testQueryClient}>{rerenderUi}</QueryClientProvider> ), };}测试策略总结| 测试类型 | 工具 | 适用场景 || ------- | -------------------------------------- | ------------ || 单元测试 | Jest + axios-mock-adapter | 测试 API 函数 || 集成测试 | MSW | 测试组件与 API 交互 || 组件测试 | React Testing Library / Vue Test Utils | 测试 UI 组件 || Hook 测试 | React Testing Library | 测试自定义 Hooks || E2E 测试 | Cypress / Playwright | 端到端测试 |最佳实践使用 MSW:推荐用于集成测试,更接近真实环境分离关注点:测试逻辑和 UI 分离清理副作用:每次测试后清理 mocks测试错误场景:不仅要测试成功,也要测试失败避免真实请求:测试时不应发送真实 HTTP 请求使用数据属性:使用 data-testid 选择元素保持测试独立:每个测试应该独立运行