• AaronConlon.dev
  • Talk
  • Weekly
  • 工具
  • 编程
  • 翻译
  • 资讯
  • 通识

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

2025年06月10上次更新于 大约 1 年前
编程

从入门到放弃再到成功:一个前端程序员的后端 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: 'test@example.com', 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
已读 0%
文章推荐
文章封面
Javascript-ES6-函数式编程阅读随写
更新于 2024-08-08
编程
文章封面
TypeScript 中的 never、unknown 的使用时机
更新于 2024-09-24
编程
GitHubTwitter / XMediumDevJuejin

友情链接

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 项目成员,开源爱好者。