从入门到放弃再到成功:一个前端程序员的后端 API 调试血泪史

2025年06月10上次更新于 11 天前
编程

从入门到放弃再到成功:一个前端程序员的后端 API 调试血泪史

序言:这是一个真实的技术调试故事,记录了一个主要从事前端开发的程序员,在 AI 助手的帮助下,如何一步步解决 Chrome 扩展后端 API 的集成测试问题。从最初的 18/31 测试失败,到最终的 31/31 全部通过,这个过程充满了挫折、学习和成长。


第一章:问题的开始 - "为什么手动测试正常,自动化测试全挂?"

我叫小李,是一名有着3年经验的前端开发工程师。最近在开发一个基于 Plasmo 框架的 Chrome 扩展,前端部分我驾轻就熟,但后端 API 对我来说还是个相对陌生的领域。

项目采用了比较现代的技术栈:

  • 后端: Hono + TypeScript + Zod + Cloudflare Workers
  • 前端: React + TypeScript + Tailwind CSS
  • 测试: Vitest 集成测试

一切看起来都很美好,直到我开始写集成测试...

困惑的开始

那是一个周五的下午,我刚写完用户认证相关的 API 接口。手动测试一切正常:

  • 用户注册 ✅
  • 用户登录 ✅
  • 获取用户信息 ✅
  • 更新用户设置 ✅

我满心欢喜地跑了一下集成测试,结果...

❯ npm run test:integration

 FAIL  Tests failed: 18/31

**18个测试失败!**我当场懵了。手动用 Postman 测试明明都正常,为什么自动化测试全挂了?

最让我困惑的是错误信息:

Expected status 201, got 400. Response: {"success":false,"message":"用户名为必需字段","status":400}

用户名是必需字段?但是我明明在测试中生成了用户名啊!我检查了测试代码:

const testUser = generateTestUser()
console.log('Generated user:', testUser) // { username: 'test_123', email: '[email protected]', password: 'password123' }

const response = await client.register(testUser)

数据明明都有,为什么服务器收到的是 undefined?

求助 AI:第一次对话

绝望之中,我决定向 AI 求助。我详细描述了问题:

"我的 Chrome 扩展 API 集成测试失败了,18/31 个测试不通过。奇怪的是,手动测试都正常,但自动化测试时服务器总是收到 undefined 的字段值。使用的是 Hono + TypeScript + Zod + Vitest,你能帮我分析一下可能的原因吗?"

AI 的回答让我眼前一亮:

"这个问题很可能是测试环境配置问题。在 Node.js 环境中运行测试时,可能缺少 fetch polyfill,或者请求头配置有问题。让我们先检查 Vitest 配置..."


第二章:环境配置的坑 - "Node.js 没有原生 fetch?"

第一个发现:缺少 fetch polyfill

AI 指导我检查 vitest.config.ts 文件,我发现确实没有配置 setupFiles:

// vitest.config.ts - 修复前
export default defineConfig({
  test: {
    environment: 'node',
    // 缺少 setupFiles 配置!
  }
})

原来 Node.js 环境下需要手动引入 fetch polyfill!AI 建议我:

  1. 安装 undici
  2. 创建 tests/setup.ts 文件
  3. 配置全局 fetch
npm install --save-dev undici
// tests/setup.ts
import { fetch, Headers, Request, Response } from 'undici';

global.fetch = fetch as any;
global.Headers = Headers as any;
global.Request = Request as any;
global.Response = Response as any;
// vitest.config.ts - 修复后
export default defineConfig({
  test: {
    environment: 'node',
    setupFiles: ['./tests/setup.ts']  // 添加这行
  }
})

我兴奋地重新运行测试,结果...还是失败!但这次错误信息变了:

TypeError: Cannot read properties of undefined (reading 'headers')

这说明至少 fetch 调用生效了,但还有其他问题。

第二个发现:请求头配置问题

AI 继续帮我分析,发现了 API 客户端中的一个细微但致命的问题:

// api-client.ts - 有问题的版本
async request(endpoint: string, options: RequestOptions = {}) {
  const config: RequestInit = {
    method: options.method || 'GET',
    ...options.headers,  // 这里有问题!当 headers 为 undefined 时会出错
    body: options.body ? JSON.stringify(options.body) : undefined,
  }
}

AI 解释说:"当 options.headers 为 undefined 时,...undefined 会导致问题。应该使用 ...(options.headers || {}) 来确保始终展开一个对象。"

修复后的代码:

// api-client.ts - 修复后
async request(endpoint: string, options: RequestOptions = {}) {
  const config: RequestInit = {
    method: options.method || 'GET',
    body: options.body ? JSON.stringify(options.body) : undefined,
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})  // 修复:确保 headers 不为 undefined
    }
  }
}

再次运行测试,这次有了重大突破!很多测试开始返回 201 Created 状态码,说明请求终于能正确发送到服务器了。

但是,新的问题出现了...


第三章:Schema 验证的噩梦 - "数据结构不匹配"

问题转移:从请求到响应

虽然请求问题解决了,但现在测试失败的原因变成了响应验证问题:

ZodError: [
  {
    "code": "invalid_type",
    "expected": "object",
    "received": "undefined",
    "path": ["message"],
    "message": "Required"
  }
]

AI 帮我分析:"现在问题是响应结构与测试预期不匹配。服务器返回的数据缺少某些字段,或者字段结构与 Zod schema 定义不一致。"

我开始逐个检查 API 响应:

/auth/me 接口的问题

测试期望的响应结构:

{
  success: true,
  message: string,
  data: {
    user: UserObject
  }
}

但实际的服务器响应:

{
  success: true,
  data: UserObject,  // 缺少包装对象
  timestamp: "2024-01-01T00:00:00.000Z"
  // 缺少 message 字段
}

AI 指导我修复服务器端代码:

// 修复前
return c.json({
  success: true,
  data: user,
  timestamp: new Date().toISOString()
})

// 修复后
return c.json({
  success: true,
  data: {
    user  // 包装在对象中
  },
  message: "获取用户信息成功",  // 添加 message
  timestamp: new Date().toISOString()
})

可用性检查接口的问题

类似的问题也出现在用户名和邮箱可用性检查接口上。AI 建议我统一添加 message 字段:

// 用户名可用性检查 - 修复后
return c.json({
  success: true,
  data: {
    username,
    available: !exists
  },
  message: "用户名可用性检查完成",  // 新增
  timestamp: new Date().toISOString()
})

但修复这些之后,还有一个更复杂的问题等着我...


第四章:业务逻辑的挑战 - "receiveOfficialMessages 到底放哪里?"

意外的需求理解错误

在修复测试的过程中,AI 发现了一个我在需求理解上的错误。我把 receiveOfficialMessages 当作了一个纯 UI 设置,但实际上它应该是:

  1. 用户表字段:存储在数据库用户表中
  2. 注册时可设置:用户注册时可以选择是否接收官方消息
  3. 设置中可修改:后续可以在设置页面中修改

AI 耐心地为我解释了这个概念:

"在实际应用中,receiveOfficialMessages 这类字段通常既是用户属性,也是用户设置。它需要存储在用户表中,因为它影响系统行为(比如是否发送邮件通知),而不仅仅是 UI 偏好设置。"

数据库 Schema 调整

首先需要修改用户 Schema:

// user.ts - 修复前
export const UserSchema = z.object({
  id: z.string().uuid(),
  username: z.string(),
  email: z.string().email(),
  isSponsored: z.boolean(),
  settings: UserSettingsSchema,  // receiveOfficialMessages 错误地放在这里
  createdAt: z.string(),
  updatedAt: z.string()
})

// user.ts - 修复后
export const UserSchema = z.object({
  id: z.string().uuid(),
  username: z.string(),
  email: z.string().email(),
  isSponsored: z.boolean(),
  receiveOfficialMessages: z.boolean(),  // 提升到用户级别
  settings: UserSettingsSchema,
  createdAt: z.string(),
  updatedAt: z.string()
})

服务器逻辑的复杂性

最复杂的部分是修改 updateUserSettings 方法。AI 指导我实现了一个动态 SQL 更新的方案:

// AuthService.updateUserSettings - 修复后
async updateUserSettings(userId: string, settings: any) {
  // 分离用户级设置和UI设置
  const { receiveOfficialMessages, ...uiSettings } = settings
  
  // 动态构建查询
  let updateQuery = "UPDATE users SET settings = ?, updated_at = ?"
  const params = [JSON.stringify(uiSettings), new Date().toISOString()]
  
  // 如果包含用户级设置,添加到查询中
  if (receiveOfficialMessages !== undefined) {
    updateQuery += ", receive_official_messages = ?"
    params.push(receiveOfficialMessages)
  }
  
  updateQuery += " WHERE id = ?"
  params.push(userId)
  
  await this.db.prepare(updateQuery).bind(...params).run()
}

AI 解释说:"这种方法的好处是,可以灵活地更新不同类型的设置,而不需要写很多重复的代码。"


第五章:细节决定成败 - "魔鬼在细节中"

错误消息的小问题

经过前面的大修改,测试通过率已经从 18/31 提升到了 30/31,只剩下最后一个测试失败:

Expected error message to contain "token", but got "无效的认证令牌"

AI 指出这是一个简单的字符串匹配问题:

// 修复前
throw new HTTPException(401, { message: "无效的认证令牌" })

// 修复后  
throw new HTTPException(401, { message: "无效的token" })

看起来微不足道,却是自动化测试的关键点。AI 说:"在编写测试时,错误消息的一致性很重要。这不仅仅是为了通过测试,也是为了给前端提供一致的用户体验。"

输入验证的完善

还有一个问题是用户名和邮箱的格式验证。测试中生成的用户名 available_${Date.now()} 长度超过了20个字符的限制。

AI 建议我在服务器端添加完整的输入验证:

// 用户名验证
if (username.length < 3 || username.length > 20 || !/^[a-zA-Z0-9_]+$/.test(username)) {
  throw new HTTPException(400, { message: "用户名格式不正确" })
}

// 邮箱验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
  throw new HTTPException(400, { message: "邮箱格式不正确" })
}

同时修改测试中的用户名生成策略:

// 修复前:用户名太长
const newUsername = `available_${Date.now()}`

// 修复后:控制长度
const newUsername = `avail${Date.now().toString().slice(-8)}`

第六章:胜利的曙光 - "31/31 全部通过!"

最后的调试

经过几轮修复,我再次运行测试:

❯ npm run test:integration

 ✓ tests/integration/basic.test.ts (11) 334ms
 ✓ tests/integration/auth.test.ts (20) 647ms

 Test Files  2 passed (2)
      Tests  31 passed (31)

31/31 全部通过!

我激动得差点从椅子上跳起来。从最初的 18/31 失败,到现在的 31/31 全部通过,这个过程虽然充满挫折,但收获巨大。

回顾整个调试过程

AI 帮我总结了这次调试的关键收获:

技术层面

  1. 环境配置:Node.js 测试环境需要正确配置 fetch polyfill
  2. 请求处理:注意 undefined 值的处理,特别是在对象展开时
  3. 响应结构:API 响应结构必须与前端期望完全一致
  4. 数据验证:服务器端输入验证是必需的
  5. 业务逻辑:理解数据应该存储在哪里(用户表 vs 设置表)

流程层面

  1. 手动测试 vs 自动化测试:两者测试的环境和方式不同,可能暴露不同的问题
  2. 逐步调试:先解决环境问题,再解决逻辑问题
  3. 错误信息解读:仔细分析错误信息,每一个细节都可能是线索
  4. Schema 一致性:前后端的数据结构定义必须保持一致

第七章:经验总结 - "从前端到全栈的思维转变"

作为前端程序员的感悟

这次经历让我深刻体会到了前端和后端开发的区别:

前端思维 vs 后端思维

前端思维

  • 关注用户界面和交互体验
  • 数据通常是"拿来即用"
  • 错误处理主要是 UI 反馈

后端思维

  • 关注数据的完整性和安全性
  • 需要考虑数据存储、验证、权限等
  • 错误处理需要考虑各种边界情况

AI 在这个过程中就像一个经验丰富的后端导师,不仅帮我解决具体问题,还帮我理解背后的原理。

自动化测试的价值

以前我觉得写测试很麻烦,手动测试不是挺好的吗?但这次经历让我认识到:

  1. 自动化测试能发现手动测试发现不了的问题
  2. 测试环境和生产环境的差异很重要
  3. 好的测试不仅验证功能,还能验证错误处理
  4. 测试是活文档,描述了 API 的预期行为

AI 辅助开发的感受

在这个过程中,AI 不仅仅是一个代码生成工具,更像是:

  • 问题诊断专家:能从错误信息中快速定位问题
  • 架构顾问:帮我理解数据应该如何组织
  • 代码审查员:发现我代码中的细微错误
  • 学习导师:解释每个修复背后的原理

但AI也不是万能的,它需要我提供准确的问题描述和上下文信息。


结语:成长的意义

从周五下午的绝望,到周一早上的胜利,这个调试过程花了我整整一个周末。虽然累,但我收获了:

技术技能

  • 学会了配置 Node.js 测试环境
  • 理解了 HTTP 请求在不同环境下的差异
  • 掌握了 Zod schema 验证的使用
  • 学会了动态 SQL 查询的构建

调试思维

  • 从现象到本质的分析能力
  • 系统性解决问题的方法
  • 错误信息的解读技巧

心态转变

  • 从害怕后端到愿意深入了解
  • 从依赖手动测试到重视自动化
  • 从单打独斗到善用 AI 工具

最重要的是,我意识到学习是一个持续的过程。技术在快速发展,工具在不断更新,但解决问题的方法论是相通的:

  1. 准确描述问题
  2. 系统性分析原因
  3. 逐步验证假设
  4. 总结经验教训

现在,每当我遇到新的技术难题时,我都会想起这次调试经历。它告诉我:没有解决不了的问题,只有还没找到对的方法

而有了AI这样的工具,我们不再是孤军奋战。它就像一个24小时在线的技术导师,帮助我们更快地学习和成长。


后记:如果你也是一名前端程序员,正在向全栈发展的路上,希望这个故事能给你一些启发。记住,每一个bug都是学习的机会,每一次调试都是成长的过程。

Keep coding, keep learning, keep growing! 🚀


项目信息

  • 技术栈:Hono + TypeScript + Zod + Vitest + Cloudflare Workers
  • 测试框架:Vitest 集成测试
  • 开发工具:AI 辅助编程
  • 最终结果:31/31 测试通过 (100% 成功率)
not-by-ainot-by-ai
文章推荐

Friends

Jimmy
Jimmy
老胡
老胡
Submara
Submara
Bruce Song
Bruce Song
Scarsu
Scarsu
宇阳
宇阳
Steven Lynn's Blog
Steven Lynn's Blog
OJ·Jimmy (Other Jimmy)
OJ·Jimmy (Other Jimmy)
liruifengv - Web 开发者,Astro 项目成员,开源爱好者。
liruifengv - Web 开发者,Astro 项目成员,开源爱好者。