beervid-app-cli 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.mjs +165 -73
- package/dist/index.d.ts +255 -0
- package/dist/index.mjs +1 -0
- package/package.json +15 -2
- package/skills/beervid-app-cli/FAQ.md +214 -0
- package/skills/beervid-app-cli/QUICKSTART.md +206 -0
- package/skills/beervid-app-cli/SKILL.md +52 -9
- package/skills/beervid-app-cli/docs/database-schema.md +12 -2
- package/skills/beervid-app-cli/docs/oauth-callback.md +92 -40
- package/skills/beervid-app-cli/docs/performance-and-limits.md +153 -0
- package/skills/beervid-app-cli/docs/security-best-practices.md +132 -0
- package/skills/beervid-app-cli/docs/testing-guide.md +689 -0
- package/skills/beervid-app-cli/docs/troubleshooting.md +468 -0
- package/skills/beervid-app-cli/example/express/README.md +6 -0
- package/skills/beervid-app-cli/example/express/server.ts +31 -9
- package/skills/beervid-app-cli/example/nextjs/README.md +6 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/oauth/callback/route.ts +22 -6
- package/skills/beervid-app-cli/example/nextjs/app/api/oauth/url/route.ts +3 -2
- package/skills/beervid-app-cli/example/nextjs/app/api/publish/tts/route.ts +2 -2
- package/skills/beervid-app-cli/example/standard/README.md +6 -0
- package/skills/beervid-app-cli/example/standard/get-oauth-url.ts +3 -2
- package/skills/beervid-app-cli/example/standard/tts-publish-flow.ts +1 -1
- package/skills/beervid-app-cli/references/api-reference.md +37 -11
- package/skills/beervid-app-cli/skill.json +36 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
# 测试指南
|
|
2
|
+
|
|
3
|
+
本文档提供 BEERVID Open API 集成的测试策略和最佳实践。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
1. [测试策略](#测试策略)
|
|
8
|
+
2. [单元测试](#单元测试)
|
|
9
|
+
3. [集成测试](#集成测试)
|
|
10
|
+
4. [端到端测试](#端到端测试)
|
|
11
|
+
5. [Mock 数据](#mock-数据)
|
|
12
|
+
6. [测试环境配置](#测试环境配置)
|
|
13
|
+
7. [CI/CD 集成](#cicd-集成)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 测试策略
|
|
18
|
+
|
|
19
|
+
### 测试金字塔
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/\
|
|
23
|
+
/E2E\ 少量端到端测试
|
|
24
|
+
/------\
|
|
25
|
+
/ 集成 \ 适量集成测试
|
|
26
|
+
/----------\
|
|
27
|
+
/ 单元测试 \ 大量单元测试
|
|
28
|
+
/--------------\
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 测试覆盖目标
|
|
32
|
+
|
|
33
|
+
| 层级 | 覆盖率目标 | 重点 |
|
|
34
|
+
|------|-----------|------|
|
|
35
|
+
| 单元测试 | > 80% | 业务逻辑、工具函数 |
|
|
36
|
+
| 集成测试 | > 60% | API 调用、数据流 |
|
|
37
|
+
| 端到端测试 | 核心流程 | 关键用户路径 |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 单元测试
|
|
42
|
+
|
|
43
|
+
### 1. 测试工具函数
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// src/utils/validation.ts
|
|
47
|
+
export function validateBusinessId(id: string): boolean {
|
|
48
|
+
return /^[a-zA-Z0-9_-]+$/.test(id) && id.length > 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function truncateProductTitle(title: string, maxLength = 30): string {
|
|
52
|
+
return title.length > maxLength ? title.slice(0, maxLength) : title
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// tests/utils/validation.test.ts
|
|
58
|
+
import { describe, it, expect } from 'vitest'
|
|
59
|
+
import { validateBusinessId, truncateProductTitle } from '@/utils/validation'
|
|
60
|
+
|
|
61
|
+
describe('validateBusinessId', () => {
|
|
62
|
+
it('should accept valid business IDs', () => {
|
|
63
|
+
expect(validateBusinessId('biz_123')).toBe(true)
|
|
64
|
+
expect(validateBusinessId('7281234567890')).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should reject invalid business IDs', () => {
|
|
68
|
+
expect(validateBusinessId('')).toBe(false)
|
|
69
|
+
expect(validateBusinessId('biz@123')).toBe(false)
|
|
70
|
+
expect(validateBusinessId('biz 123')).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('truncateProductTitle', () => {
|
|
75
|
+
it('should not truncate short titles', () => {
|
|
76
|
+
expect(truncateProductTitle('Short Title')).toBe('Short Title')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should truncate long titles', () => {
|
|
80
|
+
const longTitle = 'This is a very long product title that exceeds the limit'
|
|
81
|
+
expect(truncateProductTitle(longTitle)).toHaveLength(30)
|
|
82
|
+
expect(truncateProductTitle(longTitle)).toBe(longTitle.slice(0, 30))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should respect custom max length', () => {
|
|
86
|
+
expect(truncateProductTitle('Hello World', 5)).toBe('Hello')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. 测试 API 客户端
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// tests/client/api.test.ts
|
|
95
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
96
|
+
import { openApiGet, openApiPost } from '@/client'
|
|
97
|
+
|
|
98
|
+
// Mock fetch
|
|
99
|
+
global.fetch = vi.fn()
|
|
100
|
+
|
|
101
|
+
describe('openApiGet', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
vi.clearAllMocks()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should make GET request with correct headers', async () => {
|
|
107
|
+
const mockResponse = {
|
|
108
|
+
code: 0,
|
|
109
|
+
success: true,
|
|
110
|
+
data: { url: 'https://example.com' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
;(fetch as any).mockResolvedValueOnce({
|
|
114
|
+
ok: true,
|
|
115
|
+
json: async () => mockResponse
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const result = await openApiGet('/api/v1/open/test')
|
|
119
|
+
|
|
120
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
121
|
+
expect.stringContaining('/api/v1/open/test'),
|
|
122
|
+
expect.objectContaining({
|
|
123
|
+
method: 'GET',
|
|
124
|
+
headers: expect.objectContaining({
|
|
125
|
+
'X-API-KEY': expect.any(String)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
expect(result).toEqual(mockResponse.data)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should throw error on API failure', async () => {
|
|
134
|
+
;(fetch as any).mockResolvedValueOnce({
|
|
135
|
+
ok: true,
|
|
136
|
+
json: async () => ({
|
|
137
|
+
code: 400,
|
|
138
|
+
success: false,
|
|
139
|
+
message: 'Bad Request'
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await expect(openApiGet('/api/v1/open/test')).rejects.toThrow('Bad Request')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 3. 测试业务逻辑
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// tests/workflows/tt-publish.test.ts
|
|
152
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
153
|
+
import { publishTTFlow } from '@/workflows'
|
|
154
|
+
|
|
155
|
+
vi.mock('@/client', () => ({
|
|
156
|
+
getUploadToken: vi.fn().mockResolvedValue('token_123'),
|
|
157
|
+
uploadVideo: vi.fn().mockResolvedValue('https://cdn.beervid.ai/video.mp4'),
|
|
158
|
+
publishVideo: vi.fn().mockResolvedValue('share_abc'),
|
|
159
|
+
pollStatus: vi.fn().mockResolvedValue({
|
|
160
|
+
status: 'PUBLISH_COMPLETE',
|
|
161
|
+
post_ids: ['7123456789012345678']
|
|
162
|
+
}),
|
|
163
|
+
queryVideo: vi.fn().mockResolvedValue({
|
|
164
|
+
playCount: 100,
|
|
165
|
+
likeCount: 10
|
|
166
|
+
})
|
|
167
|
+
}))
|
|
168
|
+
|
|
169
|
+
describe('publishTTFlow', () => {
|
|
170
|
+
it('should complete full TT publish flow', async () => {
|
|
171
|
+
const result = await publishTTFlow({
|
|
172
|
+
businessId: 'biz_123',
|
|
173
|
+
file: new File([''], 'video.mp4'),
|
|
174
|
+
caption: 'Test video'
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(result).toMatchObject({
|
|
178
|
+
success: true,
|
|
179
|
+
videoId: '7123456789012345678',
|
|
180
|
+
stats: expect.objectContaining({
|
|
181
|
+
playCount: 100,
|
|
182
|
+
likeCount: 10
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 集成测试
|
|
192
|
+
|
|
193
|
+
### 1. 测试 API 集成
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// tests/integration/api.test.ts
|
|
197
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
198
|
+
import { openApiGet } from '@/client'
|
|
199
|
+
|
|
200
|
+
// 使用测试环境的真实 API
|
|
201
|
+
describe('API Integration', () => {
|
|
202
|
+
beforeAll(() => {
|
|
203
|
+
// 确保使用测试环境配置
|
|
204
|
+
process.env.BEERVID_APP_KEY = process.env.TEST_API_KEY
|
|
205
|
+
process.env.BEERVID_APP_BASE_URL = 'https://test.beervid.ai'
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should get TT OAuth URL', async () => {
|
|
209
|
+
const url = await openApiGet<string>('/api/v1/open/thirdparty-auth/tt-url')
|
|
210
|
+
|
|
211
|
+
expect(url).toMatch(/^https:\/\/www\.tiktok\.com/)
|
|
212
|
+
expect(url).toContain('client_key')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should handle invalid API key', async () => {
|
|
216
|
+
process.env.BEERVID_APP_KEY = 'invalid_key'
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
openApiGet('/api/v1/open/thirdparty-auth/tt-url')
|
|
220
|
+
).rejects.toThrow(/401|Unauthorized/)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 2. 测试数据库集成
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// tests/integration/database.test.ts
|
|
229
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
230
|
+
import { db } from '@/lib/db'
|
|
231
|
+
|
|
232
|
+
describe('Database Integration', () => {
|
|
233
|
+
beforeEach(async () => {
|
|
234
|
+
// 清理测试数据
|
|
235
|
+
await db.accounts.deleteMany({ where: { userId: 'test_user' } })
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
afterEach(async () => {
|
|
239
|
+
// 清理测试数据
|
|
240
|
+
await db.accounts.deleteMany({ where: { userId: 'test_user' } })
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should save and retrieve account', async () => {
|
|
244
|
+
const account = await db.accounts.create({
|
|
245
|
+
data: {
|
|
246
|
+
userId: 'test_user',
|
|
247
|
+
accountType: 'TT',
|
|
248
|
+
accountId: 'biz_123',
|
|
249
|
+
username: 'test_account'
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
expect(account.id).toBeDefined()
|
|
254
|
+
|
|
255
|
+
const retrieved = await db.accounts.findUnique({
|
|
256
|
+
where: { id: account.id }
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
expect(retrieved).toMatchObject({
|
|
260
|
+
userId: 'test_user',
|
|
261
|
+
accountType: 'TT',
|
|
262
|
+
accountId: 'biz_123'
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 端到端测试
|
|
271
|
+
|
|
272
|
+
### 1. 使用 Playwright
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// tests/e2e/publish-flow.spec.ts
|
|
276
|
+
import { test, expect } from '@playwright/test'
|
|
277
|
+
|
|
278
|
+
test.describe('TT Publish Flow', () => {
|
|
279
|
+
test('should complete full publish flow', async ({ page }) => {
|
|
280
|
+
// 登录
|
|
281
|
+
await page.goto('/login')
|
|
282
|
+
await page.fill('[name="email"]', 'test@example.com')
|
|
283
|
+
await page.fill('[name="password"]', 'password')
|
|
284
|
+
await page.click('button[type="submit"]')
|
|
285
|
+
|
|
286
|
+
// 等待跳转到仪表板
|
|
287
|
+
await expect(page).toHaveURL('/dashboard')
|
|
288
|
+
|
|
289
|
+
// 点击发布按钮
|
|
290
|
+
await page.click('text=发布视频')
|
|
291
|
+
|
|
292
|
+
// 上传视频
|
|
293
|
+
await page.setInputFiles('input[type="file"]', 'tests/fixtures/video.mp4')
|
|
294
|
+
|
|
295
|
+
// 填写标题
|
|
296
|
+
await page.fill('[name="caption"]', 'Test video from E2E')
|
|
297
|
+
|
|
298
|
+
// 提交发布
|
|
299
|
+
await page.click('button:has-text("发布")')
|
|
300
|
+
|
|
301
|
+
// 等待发布完成
|
|
302
|
+
await expect(page.locator('text=发布成功')).toBeVisible({ timeout: 60000 })
|
|
303
|
+
|
|
304
|
+
// 验证视频出现在列表中
|
|
305
|
+
await page.goto('/videos')
|
|
306
|
+
await expect(page.locator('text=Test video from E2E')).toBeVisible()
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 2. CLI 端到端测试
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// tests/e2e/cli.test.ts
|
|
315
|
+
import { describe, it, expect } from 'vitest'
|
|
316
|
+
import { exec } from 'child_process'
|
|
317
|
+
import { promisify } from 'util'
|
|
318
|
+
|
|
319
|
+
const execAsync = promisify(exec)
|
|
320
|
+
|
|
321
|
+
describe('CLI E2E', () => {
|
|
322
|
+
it('should show help', async () => {
|
|
323
|
+
const { stdout } = await execAsync('beervid --help')
|
|
324
|
+
expect(stdout).toContain('beervid')
|
|
325
|
+
expect(stdout).toContain('config')
|
|
326
|
+
expect(stdout).toContain('upload')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('should get OAuth URL', async () => {
|
|
330
|
+
const { stdout } = await execAsync('beervid get-oauth-url --type tt')
|
|
331
|
+
const result = JSON.parse(stdout)
|
|
332
|
+
expect(result.url).toMatch(/^https:\/\/www\.tiktok\.com/)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Mock 数据
|
|
340
|
+
|
|
341
|
+
### 1. Mock API 响应
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// tests/mocks/api-responses.ts
|
|
345
|
+
export const mockOAuthUrl = {
|
|
346
|
+
code: 0,
|
|
347
|
+
success: true,
|
|
348
|
+
message: 'ok',
|
|
349
|
+
data: 'https://www.tiktok.com/v2/auth/authorize?client_key=test'
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export const mockAccountInfo = {
|
|
353
|
+
code: 0,
|
|
354
|
+
success: true,
|
|
355
|
+
data: {
|
|
356
|
+
accountType: 'TT',
|
|
357
|
+
accountId: '7281234567890',
|
|
358
|
+
username: 'test_user',
|
|
359
|
+
displayName: 'Test User',
|
|
360
|
+
profileUrl: 'https://example.com/avatar.jpg',
|
|
361
|
+
followersCount: 1000
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export const mockUploadToken = {
|
|
366
|
+
code: 0,
|
|
367
|
+
success: true,
|
|
368
|
+
data: {
|
|
369
|
+
uploadToken: 'token_abc123',
|
|
370
|
+
uploadUrl: 'https://upload.beervid.ai'
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export const mockPublishResponse = {
|
|
375
|
+
code: 0,
|
|
376
|
+
success: true,
|
|
377
|
+
data: {
|
|
378
|
+
shareId: 'share_abc123'
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export const mockPollStatus = {
|
|
383
|
+
code: 0,
|
|
384
|
+
success: true,
|
|
385
|
+
data: {
|
|
386
|
+
status: 'PUBLISH_COMPLETE',
|
|
387
|
+
post_ids: ['7123456789012345678']
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export const mockVideoStats = {
|
|
392
|
+
code: 0,
|
|
393
|
+
success: true,
|
|
394
|
+
data: {
|
|
395
|
+
videos: [{
|
|
396
|
+
itemId: '7123456789012345678',
|
|
397
|
+
playCount: 1000,
|
|
398
|
+
likeCount: 100,
|
|
399
|
+
commentCount: 10,
|
|
400
|
+
shareCount: 5,
|
|
401
|
+
shareUrl: 'https://www.tiktok.com/@user/video/7123456789012345678'
|
|
402
|
+
}]
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 2. Mock 服务器(MSW)
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// tests/mocks/server.ts
|
|
411
|
+
import { setupServer } from 'msw/node'
|
|
412
|
+
import { rest } from 'msw'
|
|
413
|
+
import * as responses from './api-responses'
|
|
414
|
+
|
|
415
|
+
export const server = setupServer(
|
|
416
|
+
rest.get('https://open.beervid.ai/api/v1/open/thirdparty-auth/tt-url', (req, res, ctx) => {
|
|
417
|
+
return res(ctx.json(responses.mockOAuthUrl))
|
|
418
|
+
}),
|
|
419
|
+
|
|
420
|
+
rest.post('https://open.beervid.ai/api/v1/open/account/info', (req, res, ctx) => {
|
|
421
|
+
return res(ctx.json(responses.mockAccountInfo))
|
|
422
|
+
}),
|
|
423
|
+
|
|
424
|
+
rest.post('https://open.beervid.ai/api/v1/open/upload-token/generate', (req, res, ctx) => {
|
|
425
|
+
return res(ctx.json(responses.mockUploadToken))
|
|
426
|
+
})
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
// 测试设置
|
|
430
|
+
beforeAll(() => server.listen())
|
|
431
|
+
afterEach(() => server.resetHandlers())
|
|
432
|
+
afterAll(() => server.close())
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### 3. 使用 Mock 服务器
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// tests/integration/with-mock.test.ts
|
|
439
|
+
import { describe, it, expect } from 'vitest'
|
|
440
|
+
import { server } from '../mocks/server'
|
|
441
|
+
import { openApiGet } from '@/client'
|
|
442
|
+
|
|
443
|
+
describe('API with Mock Server', () => {
|
|
444
|
+
it('should get OAuth URL', async () => {
|
|
445
|
+
const url = await openApiGet<string>('/api/v1/open/thirdparty-auth/tt-url')
|
|
446
|
+
expect(url).toBe('https://www.tiktok.com/v2/auth/authorize?client_key=test')
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## 测试环境配置
|
|
454
|
+
|
|
455
|
+
### 1. 环境变量
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
# .env.test
|
|
459
|
+
BEERVID_APP_KEY=test_key_123
|
|
460
|
+
BEERVID_APP_BASE_URL=https://test.beervid.ai
|
|
461
|
+
DATABASE_URL=postgresql://test:test@localhost:5432/beervid_test
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### 2. Vitest 配置
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// vitest.config.ts
|
|
468
|
+
import { defineConfig } from 'vitest/config'
|
|
469
|
+
import path from 'path'
|
|
470
|
+
|
|
471
|
+
export default defineConfig({
|
|
472
|
+
test: {
|
|
473
|
+
globals: true,
|
|
474
|
+
environment: 'node',
|
|
475
|
+
setupFiles: ['./tests/setup.ts'],
|
|
476
|
+
coverage: {
|
|
477
|
+
provider: 'v8',
|
|
478
|
+
reporter: ['text', 'json', 'html'],
|
|
479
|
+
exclude: [
|
|
480
|
+
'node_modules/',
|
|
481
|
+
'tests/',
|
|
482
|
+
'**/*.test.ts',
|
|
483
|
+
'**/*.spec.ts'
|
|
484
|
+
]
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
resolve: {
|
|
488
|
+
alias: {
|
|
489
|
+
'@': path.resolve(__dirname, './src')
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### 3. 测试设置文件
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
// tests/setup.ts
|
|
499
|
+
import { beforeAll, afterAll, afterEach } from 'vitest'
|
|
500
|
+
import { server } from './mocks/server'
|
|
501
|
+
import dotenv from 'dotenv'
|
|
502
|
+
|
|
503
|
+
// 加载测试环境变量
|
|
504
|
+
dotenv.config({ path: '.env.test' })
|
|
505
|
+
|
|
506
|
+
// 设置 Mock 服务器
|
|
507
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
|
508
|
+
afterEach(() => server.resetHandlers())
|
|
509
|
+
afterAll(() => server.close())
|
|
510
|
+
|
|
511
|
+
// 全局测试超时
|
|
512
|
+
vi.setConfig({ testTimeout: 10000 })
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## CI/CD 集成
|
|
518
|
+
|
|
519
|
+
### 1. GitHub Actions
|
|
520
|
+
|
|
521
|
+
```yaml
|
|
522
|
+
# .github/workflows/test.yml
|
|
523
|
+
name: Test
|
|
524
|
+
|
|
525
|
+
on:
|
|
526
|
+
push:
|
|
527
|
+
branches: [main, develop]
|
|
528
|
+
pull_request:
|
|
529
|
+
branches: [main, develop]
|
|
530
|
+
|
|
531
|
+
jobs:
|
|
532
|
+
test:
|
|
533
|
+
runs-on: ubuntu-latest
|
|
534
|
+
|
|
535
|
+
services:
|
|
536
|
+
postgres:
|
|
537
|
+
image: postgres:15
|
|
538
|
+
env:
|
|
539
|
+
POSTGRES_PASSWORD: test
|
|
540
|
+
POSTGRES_DB: beervid_test
|
|
541
|
+
options: >-
|
|
542
|
+
--health-cmd pg_isready
|
|
543
|
+
--health-interval 10s
|
|
544
|
+
--health-timeout 5s
|
|
545
|
+
--health-retries 5
|
|
546
|
+
|
|
547
|
+
steps:
|
|
548
|
+
- uses: actions/checkout@v3
|
|
549
|
+
|
|
550
|
+
- name: Setup Node.js
|
|
551
|
+
uses: actions/setup-node@v3
|
|
552
|
+
with:
|
|
553
|
+
node-version: '22'
|
|
554
|
+
cache: 'npm'
|
|
555
|
+
|
|
556
|
+
- name: Install dependencies
|
|
557
|
+
run: npm ci
|
|
558
|
+
|
|
559
|
+
- name: Run type check
|
|
560
|
+
run: npm run typecheck
|
|
561
|
+
|
|
562
|
+
- name: Run tests
|
|
563
|
+
env:
|
|
564
|
+
BEERVID_APP_KEY: ${{ secrets.TEST_API_KEY }}
|
|
565
|
+
DATABASE_URL: postgresql://postgres:test@localhost:5432/beervid_test
|
|
566
|
+
run: npm test -- --coverage
|
|
567
|
+
|
|
568
|
+
- name: Upload coverage
|
|
569
|
+
uses: codecov/codecov-action@v3
|
|
570
|
+
with:
|
|
571
|
+
files: ./coverage/coverage-final.json
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### 2. 测试脚本
|
|
575
|
+
|
|
576
|
+
```json
|
|
577
|
+
// package.json
|
|
578
|
+
{
|
|
579
|
+
"scripts": {
|
|
580
|
+
"test": "vitest run",
|
|
581
|
+
"test:watch": "vitest",
|
|
582
|
+
"test:coverage": "vitest run --coverage",
|
|
583
|
+
"test:unit": "vitest run tests/unit",
|
|
584
|
+
"test:integration": "vitest run tests/integration",
|
|
585
|
+
"test:e2e": "playwright test"
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## 测试最佳实践
|
|
593
|
+
|
|
594
|
+
### 1. 遵循 AAA 模式
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
it('should truncate long product titles', () => {
|
|
598
|
+
// Arrange(准备)
|
|
599
|
+
const longTitle = 'This is a very long product title'
|
|
600
|
+
|
|
601
|
+
// Act(执行)
|
|
602
|
+
const result = truncateProductTitle(longTitle, 10)
|
|
603
|
+
|
|
604
|
+
// Assert(断言)
|
|
605
|
+
expect(result).toBe('This is a ')
|
|
606
|
+
expect(result).toHaveLength(10)
|
|
607
|
+
})
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### 2. 使用描述性测试名称
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
// ❌ 不好
|
|
614
|
+
it('test 1', () => { ... })
|
|
615
|
+
|
|
616
|
+
// ✅ 好
|
|
617
|
+
it('should return 401 when API key is invalid', () => { ... })
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### 3. 测试边界情况
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
describe('validateBusinessId', () => {
|
|
624
|
+
it('should handle empty string', () => {
|
|
625
|
+
expect(validateBusinessId('')).toBe(false)
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
it('should handle very long IDs', () => {
|
|
629
|
+
const longId = 'a'.repeat(1000)
|
|
630
|
+
expect(validateBusinessId(longId)).toBe(true)
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('should handle special characters', () => {
|
|
634
|
+
expect(validateBusinessId('biz@123')).toBe(false)
|
|
635
|
+
expect(validateBusinessId('biz_123')).toBe(true)
|
|
636
|
+
expect(validateBusinessId('biz-123')).toBe(true)
|
|
637
|
+
})
|
|
638
|
+
})
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 4. 避免测试实现细节
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
// ❌ 测试实现细节
|
|
645
|
+
it('should call fetch with correct URL', () => {
|
|
646
|
+
expect(fetch).toHaveBeenCalledWith('https://...')
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// ✅ 测试行为
|
|
650
|
+
it('should return OAuth URL', async () => {
|
|
651
|
+
const url = await getOAuthUrl('tt')
|
|
652
|
+
expect(url).toMatch(/^https:\/\/www\.tiktok\.com/)
|
|
653
|
+
})
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### 5. 保持测试独立
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
// ❌ 测试相互依赖
|
|
660
|
+
let sharedState: any
|
|
661
|
+
|
|
662
|
+
it('test 1', () => {
|
|
663
|
+
sharedState = { value: 1 }
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('test 2', () => {
|
|
667
|
+
expect(sharedState.value).toBe(1) // 依赖 test 1
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
// ✅ 测试独立
|
|
671
|
+
it('test 1', () => {
|
|
672
|
+
const state = { value: 1 }
|
|
673
|
+
expect(state.value).toBe(1)
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
it('test 2', () => {
|
|
677
|
+
const state = { value: 1 }
|
|
678
|
+
expect(state.value).toBe(1)
|
|
679
|
+
})
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## 相关文档
|
|
685
|
+
|
|
686
|
+
- [故障排查指南](./troubleshooting.md)
|
|
687
|
+
- [安全最佳实践](./security-best-practices.md)
|
|
688
|
+
- [性能与限流](./performance-and-limits.md)
|
|
689
|
+
- [示例工程](../example/)
|