From ac4418da1f86046c92191b026bc15b67836ab2e0 Mon Sep 17 00:00:00 2001 From: wmzy Date: Mon, 9 Mar 2026 23:33:21 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/util/http.test.ts: HTTP 工具函数测试 (8 tests) - src/services/article.test.ts: 文章服务测试 (5 tests) 使用 vitest 框架 --- src/services/article.test.ts | 75 ++++++++++++++++ src/util/http.test.ts | 162 +++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/services/article.test.ts create mode 100644 src/util/http.test.ts diff --git a/src/services/article.test.ts b/src/services/article.test.ts new file mode 100644 index 0000000..8b15c53 --- /dev/null +++ b/src/services/article.test.ts @@ -0,0 +1,75 @@ +import {describe, it, expect, vi, beforeEach} from 'vitest'; +import * as article from '@/services/article'; + +vi.mock('@/util/http', () => ({ + get: vi.fn() +})); + +import * as http from '@/util/http'; + +describe('article service', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('query', () => { + it('should call http.get with articles endpoint', async () => { + const mockData = {articles: [], articlesCount: 0}; + vi.mocked(http.get).mockResolvedValue(mockData as any); + + const result = await article.query(); + + expect(http.get).toHaveBeenCalledWith('articles', undefined); + expect(result).toEqual(mockData); + }); + + it('should pass query params to http.get', async () => { + const mockData = {articles: [], articlesCount: 0}; + vi.mocked(http.get).mockResolvedValue(mockData as any); + + const params = {limit: 10, offset: 0, tag: 'react'}; + await article.query(params); + + expect(http.get).toHaveBeenCalledWith('articles', params); + }); + }); + + describe('findByTitle', () => { + it('should fetch article by title and return article property', async () => { + const mockArticle = {title: 'Test Article', slug: 'test-article'}; + vi.mocked(http.get).mockResolvedValue({article: mockArticle} as any); + + const result = await article.findByTitle('test-article'); + + expect(http.get).toHaveBeenCalledWith('articles/test-article'); + expect(result).toEqual(mockArticle); + }); + }); + + describe('fetchCommentsByTitle', () => { + it('should fetch comments for an article', async () => { + const mockComments = [ + {id: '1', body: 'Comment 1'}, + {id: '2', body: 'Comment 2'} + ]; + vi.mocked(http.get).mockResolvedValue({comments: mockComments} as any); + + const result = await article.fetchCommentsByTitle('test-article'); + + expect(http.get).toHaveBeenCalledWith('articles/test-article/comments'); + expect(result).toEqual(mockComments); + }); + }); + + describe('fetchTags', () => { + it('should fetch tags and return tags array', async () => { + const mockTags = ['react', 'typescript', 'vitest']; + vi.mocked(http.get).mockResolvedValue({tags: mockTags} as any); + + const result = await article.fetchTags(); + + expect(http.get).toHaveBeenCalledWith('tags'); + expect(result).toEqual(mockTags); + }); + }); +}); diff --git a/src/util/http.test.ts b/src/util/http.test.ts new file mode 100644 index 0000000..d21093f --- /dev/null +++ b/src/util/http.test.ts @@ -0,0 +1,162 @@ +import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'; +import {fetchJSON, get, del, post, put} from '@/util/http'; + +describe('http utilities', () => { + let fetchMock: ReturnType; + + beforeEach(() => { + fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + describe('fetchJSON', () => { + it('should make a request with correct headers', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({data: 'test'}) + }; + fetchMock.mockResolvedValue(mockResponse); + + const result = await fetchJSON('test'); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.realworld.io/api/test', + expect.objectContaining({ + headers: expect.objectContaining({ + 'content-type': 'application/json', + accept: 'application/json' + }) + }) + ); + expect(result).toEqual({data: 'test'}); + }); + + it('should merge custom headers', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({data: 'test'}) + }; + fetchMock.mockResolvedValue(mockResponse); + + await fetchJSON('test', { + headers: {Authorization: 'Bearer token'} + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.realworld.io/api/test', + expect.objectContaining({ + headers: expect.objectContaining({ + 'content-type': 'application/json', + accept: 'application/json', + Authorization: 'Bearer token' + }) + }) + ); + }); + + it('should throw error when response is not ok', async () => { + const mockResponse = { + ok: false, + json: vi.fn().mockResolvedValue({message: 'Error message'}) + }; + fetchMock.mockResolvedValue(mockResponse); + + await expect(fetchJSON('test')).rejects.toThrow('Error message'); + }); + }); + + describe('get', () => { + it('should make GET request', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({data: 'test'}) + }; + fetchMock.mockResolvedValue(mockResponse); + + await get('articles'); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.realworld.io/api/articles', + expect.objectContaining({method: 'get'}) + ); + }); + + it('should append query string when params provided', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({data: 'test'}) + }; + fetchMock.mockResolvedValue(mockResponse); + + await get('articles', {limit: 10, offset: 0}); + + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining('limit=10'), + expect.objectContaining({method: 'get'}) + ); + }); + }); + + describe('del', () => { + it('should make DELETE request', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({success: true}) + }; + fetchMock.mockResolvedValue(mockResponse); + + await del('articles/123'); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.realworld.io/api/articles/123', + expect.objectContaining({method: 'delete'}) + ); + }); + }); + + describe('post', () => { + it('should make POST request with JSON body', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({article: {id: 1}}) + }; + fetchMock.mockResolvedValue(mockResponse); + + const data = {title: 'Test', body: 'Content'}; + await post('articles', data); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.realworld.io/api/articles', + expect.objectContaining({ + method: 'post', + body: JSON.stringify(data) + }) + ); + }); + }); + + describe('put', () => { + it('should make PUT request with JSON body', async () => { + const mockResponse = { + ok: true, + json: vi.fn().mockResolvedValue({article: {id: 1}}) + }; + fetchMock.mockResolvedValue(mockResponse); + + const data = {title: 'Updated'}; + await put('articles/123', data); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.realworld.io/api/articles/123', + expect.objectContaining({ + method: 'put', + body: JSON.stringify(data) + }) + ); + }); + }); +});