5月29日 22:54

How to perform unit testing and mocking on code using axios? Please explain common testing methods

When testing code that uses axios, you need to master unit testing, integration testing, and mocking techniques.

1. Using Jest and axios-mock-adapter

Install Dependencies

bash
npm install --save-dev jest axios-mock-adapter @testing-library/react

Basic Mock Testing

javascript
// api/user.js import 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.js import 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); }); });

Advanced Mock Configuration

javascript
// __tests__/api.test.js import 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 }]; }); // Test success const response = await axios.post('/api/users', { name: 'John', email: 'john@example.com' }); expect(response.status).toBe(201); // Test failure 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. Using MSW (Mock Service Worker)

Installation and Configuration

bash
npm install --save-dev msw
javascript
// mocks/handlers.js import { rest } from 'msw'; export const handlers = [ // GET request rest.get('/api/users', (req, res, ctx) => { return res( ctx.status(200), ctx.json([ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ]) ); }), // GET single user 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 request rest.post('/api/users', async (req, res, ctx) => { const body = await req.json(); return res( ctx.status(201), ctx.json({ id: 3, ...body }) ); }), // PUT request 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 request rest.delete('/api/users/:id', (req, res, ctx) => { return res(ctx.status(204)); }) ]; // mocks/server.js import { setupServer } from 'msw/node'; import { handlers } from './handlers'; export const server = setupServer(...handlers); // jest.setup.js import { server } from './mocks/server'; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close());

Testing with MSW

javascript
// __tests__/user.integration.test.js import { 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(); }); });

Dynamic Handler Override

javascript
// __tests__/dynamic-mock.test.js import { rest } from 'msw'; import { server } from '../mocks/server'; test('should handle server error', async () => { // Temporarily override 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 Component Testing

Using React Testing Library

javascript
// components/UserProfile.jsx import 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.jsx import 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 Component Testing

Using Vue Test Utils

javascript
// 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.js import { 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. Custom Hook/Composable Testing

javascript
// hooks/useApi.js import { 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.js import { 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 Testing

Using Cypress

javascript
// cypress/integration/api.spec.js describe('API Tests', () => { beforeEach(() => { // Intercept API requests 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. Testing Best Practices

Test File Organization

shell
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 Utility Functions

javascript
// test-utils.js import { 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> ), }; }

Testing Strategy Summary

Test TypeToolsApplicable Scenarios
Unit TestingJest + axios-mock-adapterTesting API functions
Integration TestingMSWTesting component and API interaction
Component TestingReact Testing Library / Vue Test UtilsTesting UI components
Hook TestingReact Testing LibraryTesting custom Hooks
E2E TestingCypress / PlaywrightEnd-to-end testing

Best Practices

  1. Use MSW: Recommended for integration testing, closer to real environment
  2. Separate Concerns: Separate testing logic from UI
  3. Clean Up Side Effects: Clean up mocks after each test
  4. Test Error Scenarios: Test failures as well as successes
  5. Avoid Real Requests: Should not send real HTTP requests during testing
  6. Use Data Attributes: Use data-testid to select elements
  7. Keep Tests Independent: Each test should run independently
标签:JavaScript前端Axios