# 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 的后端模块测试
