|
@@ -0,0 +1,296 @@
|
|
|
|
|
+# hono/testing testClient 调用规范
|
|
|
|
|
+
|
|
|
|
|
+## 版本信息
|
|
|
|
|
+| 版本 | 日期 | 描述 | 作者 |
|
|
|
|
|
+|------|------|------|------|
|
|
|
|
|
+| 1.0 | 2026-01-03 | 初始版本,基于故事010.003修复经验总结 | James (Claude Code) |
|
|
|
|
|
+
|
|
|
|
|
+## 概述
|
|
|
|
|
+
|
|
|
|
|
+本文档定义了使用 `hono/testing` 的 `testClient` 测试API路由时的正确调用方式,避免常见的调用错误。
|
|
|
|
|
+
|
|
|
|
|
+## 基本原理
|
|
|
|
|
+
|
|
|
|
|
+`testClient` 会根据Hono路由的路径结构自动生成类型安全的客户端对象。关键是理解路径到属性的映射规则。
|
|
|
|
|
+
|
|
|
|
|
+### 核心映射规则
|
|
|
|
|
+
|
|
|
|
|
+1. **路径分隔符 `/` 转换为嵌套对象**
|
|
|
|
|
+2. **路径参数 `:param` 转换为 `[':param']` 索引**
|
|
|
|
|
+3. **开头的 `/` 被移除**
|
|
|
|
|
+4. **连续的 `/` 被压缩为单层嵌套**
|
|
|
|
|
+
|
|
|
|
|
+## 调用方式详解
|
|
|
|
|
+
|
|
|
|
|
+### 1. 简单路由(无参数)
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 路由定义
|
|
|
|
|
+const route = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/', // ✅ 根路径
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+app.openapi(route, handler);
|
|
|
|
|
+
|
|
|
|
|
+// 测试调用
|
|
|
|
|
+const client = testClient(app);
|
|
|
|
|
+const response = await client.$get({ // ✅ 使用 .$get()
|
|
|
|
|
+ query: { page: 1 }
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 路由定义
|
|
|
|
|
+const route = createRoute({
|
|
|
|
|
+ method: 'post',
|
|
|
|
|
+ path: '/users', // 单层路径
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+app.openapi(route, handler);
|
|
|
|
|
+
|
|
|
|
|
+// 测试调用
|
|
|
|
|
+const response = await client.users.$post({ // ✅ client.users.$post()
|
|
|
|
|
+ json: userData
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2. 路由带参数
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 路由定义
|
|
|
|
|
+const route = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/:id', // 参数路径
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+app.openapi(route, handler);
|
|
|
|
|
+
|
|
|
|
|
+// 测试调用
|
|
|
|
|
+const response = await client[':id'].$get({ // ✅ client[':id'].$get()
|
|
|
|
|
+ param: { id: 123 }
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 路由定义
|
|
|
|
|
+const route = createRoute({
|
|
|
|
|
+ method: 'put',
|
|
|
|
|
+ path: '/users/:id', // 嵌套路由+参数
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+app.openapi(route, handler);
|
|
|
|
|
+
|
|
|
|
|
+// 测试调用
|
|
|
|
|
+const response = await client.users[':id'].$put({ // ✅ client.users[':id'].$put()
|
|
|
|
|
+ param: { id: 123 },
|
|
|
|
|
+ json: updateData
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3. 嵌套路由
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 路由定义
|
|
|
|
|
+const route = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/admin/users/:id', // 多层嵌套+参数
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+app.openapi(route, handler);
|
|
|
|
|
+
|
|
|
|
|
+// 测试调用
|
|
|
|
|
+const response = await client.admin.users[':id'].$get({ // ✅ 嵌套调用
|
|
|
|
|
+ param: { id: 123 }
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 常见错误 ❌
|
|
|
|
|
+
|
|
|
|
|
+### 错误1: 使用完整路径字符串
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ❌ 错误:使用完整路径作为属性
|
|
|
|
|
+const response = await client['/api/v1/users'].$get();
|
|
|
|
|
+
|
|
|
|
|
+// ✅ 正确:去掉前缀,使用相对路径映射
|
|
|
|
|
+const response = await client.users.$get();
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 错误2: 路径与模块定义不匹配
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 模块路由定义为 /:id
|
|
|
|
|
+const route = createRoute({
|
|
|
|
|
+ path: '/:id',
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// ❌ 错误:使用了完整路径
|
|
|
|
|
+const response = await client['/api/v1/:id'].$get();
|
|
|
|
|
+
|
|
|
|
|
+// ✅ 正确:使用模块内的相对路径
|
|
|
|
|
+const response = await client[':id'].$get();
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 错误3: 忘记使用 `:id` 索引
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 路由定义: path: '/users/:id'
|
|
|
|
|
+
|
|
|
|
|
+// ❌ 错误:直接访问id属性
|
|
|
|
|
+const response = await client.users[id].$get(); // 编译错误
|
|
|
|
|
+
|
|
|
|
|
+// ✅ 正确:使用字符串索引
|
|
|
|
|
+const response = await client.users[':id'].$get({
|
|
|
|
|
+ param: { id }
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 错误4: 嵌套路由使用错误的分隔方式
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 路由定义: path: '/admin/users/:id'
|
|
|
|
|
+
|
|
|
|
|
+// ❌ 错误:用斜杠分隔
|
|
|
|
|
+const response = await client['admin/users/:id'].$get();
|
|
|
|
|
+
|
|
|
|
|
+// ❌ 错误:用点号分隔参数
|
|
|
|
|
+const response = await client.admin.users.:id.$get();
|
|
|
|
|
+
|
|
|
|
|
+// ✅ 正确:用嵌套对象+字符串索引
|
|
|
|
|
+const response = await client.admin.users[':id'].$get();
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 后端模块开发规范关联
|
|
|
|
|
+
|
|
|
|
|
+### 路由定义规范
|
|
|
|
|
+
|
|
|
|
|
+**模块内部路由应使用相对路径**(不以 `/` 开头的完整路径):
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ✅ 正确:模块内使用相对路径
|
|
|
|
|
+const listRoute = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/', // 或 '/:id'
|
|
|
|
|
+ middleware: [authMiddleware],
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// ❌ 错误:模块内使用完整API路径
|
|
|
|
|
+const listRoute = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/api/v1/admin/users', // 不应在模块内包含前缀
|
|
|
|
|
+ middleware: [authMiddleware],
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Server包注册时添加完整前缀**:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// packages/server/src/index.ts
|
|
|
|
|
+import { unifiedAdvertisementAdminRoutes } from '@d8d/unified-advertisements-module';
|
|
|
|
|
+
|
|
|
|
|
+// ✅ 正确:Server包注册时添加完整前缀
|
|
|
|
|
+app.route('/api/v1/admin/unified-advertisements', unifiedAdvertisementAdminRoutes);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 测试调用规范
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ✅ 正确:测试时使用模块的相对路径
|
|
|
|
|
+const adminClient = testClient(unifiedAdvertisementAdminRoutes);
|
|
|
|
|
+
|
|
|
|
|
+// 路由定义: path: '/'
|
|
|
|
|
+const response = await adminClient.$get({
|
|
|
|
|
+ query: { page: 1 }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 路由定义: path: '/:id'
|
|
|
|
|
+const response = await adminClient[':id'].$put({
|
|
|
|
|
+ param: { id: 123 },
|
|
|
|
|
+ json: data
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 完整示例对照表
|
|
|
|
|
+
|
|
|
|
|
+| 路由定义 (模块内) | Server注册 | 测试调用方式 |
|
|
|
|
|
+|-----------------|-----------|-------------|
|
|
|
|
|
+| `path: '/'` | `.route('/api/v1/users', routes)` | `client.$get()` |
|
|
|
|
|
+| `path: '/users'` | `.route('/api/v1/users', routes)` | `client.users.$get()` |
|
|
|
|
|
+| `path: '/:id'` | `.route('/api/v1/users', routes)` | `client[':id'].$get()` |
|
|
|
|
|
+| `path: '/users/:id'` | `.route('/api/v1/users', routes)` | `client.users[':id'].$get()` |
|
|
|
|
|
+| `path: '/admin/users/:id'` | `.route('/api/v1/admin', routes)` | `client.admin.users[':id'].$get()` |
|
|
|
|
|
+
|
|
|
|
|
+## 调用技巧
|
|
|
|
|
+
|
|
|
|
|
+### 1. 使用 TypeScript 类型提示
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const client = testClient<typeof userRoutes>(userRoutes);
|
|
|
|
|
+
|
|
|
|
|
+// IDE会自动补全可用的方法和路径
|
|
|
|
|
+client.$get() // 如果路由有GET /
|
|
|
|
|
+client.users.$get() // 如果路由有GET /users
|
|
|
|
|
+client[':id'].$get() // 如果路由有GET /:id
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2. 查看路由定义确认调用方式
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 查看路由文件中的 path 定义
|
|
|
|
|
+// src/routes/users.ts
|
|
|
|
|
+const listRoute = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/', // ← 这里决定了测试调用方式
|
|
|
|
|
+ // ...
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 根据path确定测试调用
|
|
|
|
|
+await client.$get() // path: '/' → .$get()
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3. 调试错误信息
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 如果看到类似错误:
|
|
|
|
|
+// "Cannot read property '$get' of undefined"
|
|
|
|
|
+// 说明路径映射不正确
|
|
|
|
|
+
|
|
|
|
|
+// 检查:
|
|
|
|
|
+// 1. 路由 path 是否正确定义
|
|
|
|
|
+// 2. testClient 是否使用了正确的路由实例
|
|
|
|
|
+// 3. 是否有多余的路径前缀
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 总结
|
|
|
|
|
+
|
|
|
|
|
+### 快速参考
|
|
|
|
|
+
|
|
|
|
|
+| 路由 path | testClient 调用 |
|
|
|
|
|
+|-----------|-----------------|
|
|
|
|
|
+| `/` | `.$get()`, `.$post()`, etc. |
|
|
|
|
|
+| `/:id` | `[':id'].$get()`, `[':id'].$put()`, etc. |
|
|
|
|
|
+| `/users` | `.users.$get()`, `.users.$post()`, etc. |
|
|
|
|
|
+| `/users/:id` | `.users[':id'].$get()`, `.users[':id'].$put()`, etc. |
|
|
|
|
|
+| `/admin/users/:id` | `.admin.users[':id'].$get()`, etc. |
|
|
|
|
|
+
|
|
|
|
|
+### 核心原则
|
|
|
|
|
+
|
|
|
|
|
+1. **模块内使用相对路径**:`path: '/'` 或 `path: '/:id'`
|
|
|
|
|
+2. **Server注册时添加前缀**:`.route('/api/v1/xxx', routes)`
|
|
|
|
|
+3. **测试时映射路径结构**:`/` → `.$method()`, `/:id` → `[':id'].$method()`
|
|
|
|
|
+4. **嵌套路径反映为嵌套对象**:`/admin/users/:id` → `.admin.users[':id'].$method()`
|
|
|
|
|
+
|
|
|
|
|
+## 相关文档
|
|
|
|
|
+
|
|
|
|
|
+- [后端模块包开发规范](./backend-module-package-standards.md) - 路由定义规范
|
|
|
|
|
+- [后端模块包测试规范](./backend-module-testing-standards.md) - 测试框架和集成测试
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**文档状态**: 正式版
|
|
|
|
|
+**创建原因**: 故事010.003修复过程中发现缺乏testClient调用规范
|
|
|
|
|
+**适用范围**: 所有使用 hono/testing 的后端模块测试
|