/** * APIService 模块单元测试 * 覆盖 HTTP 请求、重试、缓存、去重等核心功能 */ import APIService from '../../src/js/modules/api-service'; describe('APIService', () => { let service; beforeEach(() => { service = new APIService({ baseURL: 'https://api.test.com', timeout: 5000, retries: 2 }); fetch.mockReset(); jest.useRealTimers(); }); describe('构造函数', () => { it('应该使用默认配置', () => { const defaultService = new APIService(); expect(defaultService.baseURL).toBe(''); expect(defaultService.timeout).toBe(30000); expect(defaultService.retries).toBe(3); }); it('应该使用自定义配置', () => { expect(service.baseURL).toBe('https://api.test.com'); expect(service.timeout).toBe(5000); expect(service.retries).toBe(2); }); }); describe('GET 请求', () => { it('应该发送 GET 请求并返回 JSON', async () => { const mockData = { id: 1, name: 'test' }; fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve(mockData) }); const result = await service.get('/users'); expect(result).toEqual(mockData); expect(fetch).toHaveBeenCalledWith( 'https://api.test.com/users', expect.objectContaining({ method: 'GET' }) ); }); it('应该处理非 JSON 响应', async () => { fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'text/plain' }, text: () => Promise.resolve('hello') }); const result = await service.get('/text'); expect(result).toBe('hello'); }); }); describe('POST 请求', () => { it('应该发送 POST 请求并携带 body', async () => { const body = { name: 'test' }; fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ success: true }) }); const result = await service.post('/create', body); expect(result).toEqual({ success: true }); const callArgs = fetch.mock.calls[0]; expect(callArgs[1].method).toBe('POST'); expect(callArgs[1].body).toBe(JSON.stringify(body)); }); }); describe('PUT 请求', () => { it('应该发送 PUT 请求', async () => { fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ updated: true }) }); await service.put('/update', { id: 1 }); expect(fetch.mock.calls[0][1].method).toBe('PUT'); }); }); describe('DELETE 请求', () => { it('应该发送 DELETE 请求', async () => { fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ deleted: true }) }); await service.delete('/remove'); expect(fetch.mock.calls[0][1].method).toBe('DELETE'); }); }); describe('错误处理', () => { it('应该为每次重试创建新的超时信号', async () => { const timeoutService = new APIService({ baseURL: 'https://api.test.com', timeout: 5000, retries: 2 }); const firstSignal = { aborted: true }; const secondSignal = { aborted: false }; const createTimeoutSpy = jest.spyOn(timeoutService, 'createTimeoutSignal') .mockReturnValueOnce({ signal: firstSignal, clear: jest.fn() }) .mockReturnValueOnce({ signal: secondSignal, clear: jest.fn() }); fetch.mockImplementationOnce(() => Promise.reject(new Error('timeout'))); fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ ok: true }) }); const result = await timeoutService.get('/retry-timeout'); expect(result).toEqual({ ok: true }); expect(fetch).toHaveBeenCalledTimes(2); expect(createTimeoutSpy).toHaveBeenCalledTimes(2); expect(fetch.mock.calls[0][1].signal).toBe(firstSignal); expect(fetch.mock.calls[1][1].signal).toBe(secondSignal); }); it('应该在 HTTP 错误时抛出异常', async () => { // mock fetch 返回 error response,但重试会多次调用 fetch.mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found' }); await expect(service.get('/missing')).rejects.toThrow(); expect(fetch).toHaveBeenCalled(); }); }); describe('缓存机制', () => { it('应该缓存 GET 请求结果', async () => { fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ data: 1 }) }); await service.get('/cached'); expect(fetch).toHaveBeenCalledTimes(1); const result = await service.get('/cached'); expect(fetch).toHaveBeenCalledTimes(1); expect(result).toEqual({ data: 1 }); }); it('应该清除所有缓存', async () => { fetch.mockResolvedValue({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ data: 1 }) }); await service.get('/test'); service.clearCache(); await service.get('/test'); expect(fetch).toHaveBeenCalledTimes(2); }); it('应该清除特定端点的缓存', async () => { fetch.mockResolvedValue({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ data: 1 }) }); await service.get('/users/1'); await service.get('/users/2'); service.clearCacheEntry('/users/1'); await service.get('/users/1'); expect(fetch).toHaveBeenCalledTimes(3); }); }); describe('请求去重', () => { it('应该去重并发的相同请求', async () => { fetch.mockImplementation(() => new Promise(resolve => { setTimeout(() => resolve({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ data: 1 }) }), 100); })); const [r1, r2, r3] = await Promise.all([ service.get('/dedup'), service.get('/dedup'), service.get('/dedup') ]); expect(fetch).toHaveBeenCalledTimes(1); expect(r1).toEqual({ data: 1 }); }); }); describe('getCacheKey()', () => { it('应该生成正确的缓存键', () => { const key = service.getCacheKey('https://api.test.com/test', 'GET', null); expect(key).toBe('GET:https://api.test.com/test:'); }); it('应该包含 body 到缓存键', () => { const key = service.getCacheKey('https://api.test.com/test', 'POST', { a: 1 }); expect(key).toContain(JSON.stringify({ a: 1 })); }); }); });