# 后端模块包测试规范

## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 1.0 | 2025-12-26 | 从测试策略文档拆分，专注后端模块包测试 | James (Claude Code) |

## 概述

本文档定义了后端模块包的测试标准和最佳实践。
- **目标**: `packages/*-module` - 业务模块包（user-module、auth-module、file-module等）
- **测试类型**: 单元测试 + 集成测试

## 测试框架栈

- **Vitest**: 测试运行器
- **hono/testing**: Hono路由测试
- **TypeORM**: 数据库测试
- **PostgreSQL**: 测试数据库
- **shared-test-util**: 共享测试基础设施

## 测试分层策略

### 1. 单元测试（Unit Tests）
- **范围**: 单个服务、工具函数、Schema验证
- **目标**: 验证独立单元的正确性
- **位置**: `packages/*-module/tests/unit/**/*.test.ts`
- **框架**: Vitest
- **覆盖率目标**: ≥ 80%

### 2. 集成测试（Integration Tests）
- **范围**: 路由、服务、数据库集成
- **目标**: 验证模块内部组件协作
- **位置**: `packages/*-module/tests/integration/**/*.test.ts`
- **框架**: Vitest + hono/testing + TypeORM
- **覆盖率目标**: ≥ 60%

## 测试文件结构

```
packages/user-module/
├── src/
│   ├── entities/
│   │   ├── user.entity.ts
│   │   └── role.entity.ts
│   ├── services/
│   │   ├── user.service.ts
│   │   └── role.service.ts
│   ├── schemas/
│   │   ├── user.schema.ts
│   │   └── role.schema.ts
│   └── routes/
│       ├── user.routes.ts
│       └── role.routes.ts
└── tests/
    ├── unit/
    │   ├── services/
    │   │   ├── user.service.test.ts
    │   │   └── role.service.test.ts
    │   └── schemas/
    │       ├── user.schema.test.ts
    │       └── role.schema.test.ts
    ├── integration/
    │   ├── routes/
    │   │   ├── user.routes.integration.test.ts
    │   │   └── role.routes.integration.test.ts
    │   └── services/
    │       └── user.service.integration.test.ts
    └── fixtures/
        ├── test-db.ts        # 测试数据库配置
        └── factories.ts      # 测试数据工厂
```

## 单元测试最佳实践

### 1. 测试Service层
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from '../src/services/user.service';
import { User } from '../src/entities/user.entity';
import { Repository } from 'typeorm';

describe('UserService', () => {
  let userService: UserService;
  let mockUserRepo: Partial<Repository<User>>;

  beforeEach(() => {
    // 模拟Repository
    mockUserRepo = {
      findOne: vi.fn(),
      find: vi.fn(),
      create: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
      delete: vi.fn()
    };

    userService = new UserService(mockUserRepo as Repository<User>);
  });

  describe('findById()', () => {
    it('应该返回用户数据', async () => {
      // Arrange
      const mockUser = { id: 1, username: 'testuser', email: 'test@example.com' };
      mockUserRepo.findOne?.mockResolvedValue(mockUser);

      // Act
      const result = await userService.findById(1);

      // Assert
      expect(result).toEqual(mockUser);
      expect(mockUserRepo.findOne).toHaveBeenCalledWith({
        where: { id: 1 }
      });
    });

    it('应该返回null当用户不存在', async () => {
      mockUserRepo.findOne?.mockResolvedValue(null);

      const result = await userService.findById(999);

      expect(result).toBeNull();
    });
  });

  describe('createUser()', () => {
    it('应该创建新用户', async () => {
      const userData = {
        username: 'newuser',
        email: 'new@example.com',
        password: 'hashedpassword'
      };

      const createdUser = { id: 1, ...userData };
      mockUserRepo.create?.mockReturnValue(createdUser);
      mockUserRepo.save?.mockResolvedValue(createdUser);

      const result = await userService.createUser(userData);

      expect(result).toEqual(createdUser);
      expect(mockUserRepo.create).toHaveBeenCalledWith(userData);
      expect(mockUserRepo.save).toHaveBeenCalledWith(createdUser);
    });

    it('应该抛出错误当用户名已存在', async () => {
      mockUserRepo.findOne?.mockResolvedValue({ id: 1, username: 'existing' });

      await expect(
        userService.createUser({ username: 'existing', email: 'test@example.com', password: 'hash' })
      ).rejects.toThrow('用户名已存在');
    });
  });
});
```

### 2. 测试Schema验证
```typescript
import { describe, it, expect } from 'vitest';
import { userSchema } from '../src/schemas/user.schema';

describe('userSchema', () => {
  it('应该验证有效的用户数据', async () => {
    const validData = {
      username: 'testuser',
      email: 'test@example.com',
      password: 'password123'
    };

    const result = await userSchema.parseAsync(validData);

    expect(result).toEqual(validData);
  });

  it('应该拒绝无效的邮箱格式', async () => {
    const invalidData = {
      username: 'testuser',
      email: 'invalid-email',
      password: 'password123'
    };

    await expect(userSchema.parseAsync(invalidData)).rejects.toThrow();
  });

  it('应该拒绝空密码', async () => {
    const invalidData = {
      username: 'testuser',
      email: 'test@example.com',
      password: ''
    };

    await expect(userSchema.parseAsync(invalidData)).rejects.toThrow();
  });
});
```

### 3. 测试工具函数
```typescript
import { describe, it, expect } from 'vitest';
import { hashPassword, comparePassword } from '../src/utils/password.util';

describe('密码工具函数', () => {
  describe('hashPassword()', () => {
    it('应该返回哈希后的密码', async () => {
      const password = 'password123';
      const hash = await hashPassword(password);

      expect(hash).not.toBe(password);
      expect(hash).toMatch(/^\$2[ayb]\$.{56}$/); // bcrypt格式
    });

    it('应该为相同密码生成不同的哈希值', async () => {
      const password = 'password123';
      const hash1 = await hashPassword(password);
      const hash2 = await hashPassword(password);

      expect(hash1).not.toBe(hash2);
    });
  });

  describe('comparePassword()', () => {
    it('应该返回true当密码匹配', async () => {
      const password = 'password123';
      const hash = await hashPassword(password);

      const result = await comparePassword(password, hash);

      expect(result).toBe(true);
    });

    it('应该返回false当密码不匹配', async () => {
      const hash = await hashPassword('password123');

      const result = await comparePassword('wrongpassword', hash);

      expect(result).toBe(false);
    });
  });
});
```

## 集成测试最佳实践

### 1. 测试路由层
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { integrateRoutes } from 'hono/testing';
import { Hono } from 'hono';
import { getTestDataSource } from './fixtures/test-db';
import { userRoutes } from '../src/routes/user.routes';
import { seedTestUser } from './fixtures/factories';

describe('用户路由集成测试', () => {
  let dataSource: DataSource;
  let app: Hono;

  beforeEach(async () => {
    dataSource = await getTestDataSource();
    await dataSource.initialize();

    app = new Hono();
    app.route('/api/users', userRoutes);
  });

  afterEach(async () => {
    await dataSource.destroy();
  });

  describe('GET /api/users', () => {
    it('应该返回用户列表', async () => {
      // Arrange
      await seedTestUser(dataSource, { username: 'user1' });
      await seedTestUser(dataSource, { username: 'user2' });

      // Act
      const res = await integrateRoutes(app).GET('/api/users');

      // Assert
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.data).toHaveLength(2);
      expect(json.data[0].username).toBe('user1');
    });

    it('应该支持分页查询', async () => {
      // 创建15个用户
      for (let i = 1; i <= 15; i++) {
        await seedTestUser(dataSource, { username: `user${i}` });
      }

      // 查询第一页
      const res1 = await integrateRoutes(app).GET('/api/users?page=1&pageSize=10');
      expect(res1.status).toBe(200);
      const json1 = await res1.json();
      expect(json1.data).toHaveLength(10);
      expect(json1.total).toBe(15);

      // 查询第二页
      const res2 = await integrateRoutes(app).GET('/api/users?page=2&pageSize=10');
      expect(res2.status).toBe(200);
      const json2 = await res2.json();
      expect(json2.data).toHaveLength(5);
    });
  });

  describe('GET /api/users/:id', () => {
    it('应该返回用户详情', async () => {
      const user = await seedTestUser(dataSource, { username: 'testuser' });

      const res = await integrateRoutes(app).GET(`/api/users/${user.id}`);

      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.username).toBe('testuser');
    });

    it('应该返回404当用户不存在', async () => {
      const res = await integrateRoutes(app).GET('/api/users/99999');

      expect(res.status).toBe(404);
    });
  });

  describe('POST /api/users', () => {
    it('应该创建新用户', async () => {
      const userData = {
        username: 'newuser',
        email: 'new@example.com',
        password: 'password123'
      };

      const res = await integrateRoutes(app).POST('/api/users', { json: userData });

      expect(res.status).toBe(201);
      const json = await res.json();
      expect(json.username).toBe('newuser');

      // 验证数据库中的数据
      const userRepo = dataSource.getRepository(User);
      const user = await userRepo.findOne({ where: { username: 'newuser' } });
      expect(user).toBeDefined();
    });

    it('应该拒绝重复的用户名', async () => {
      await seedTestUser(dataSource, { username: 'existing' });

      const res = await integrateRoutes(app).POST('/api/users', {
        json: {
          username: 'existing',
          email: 'different@example.com',
          password: 'password123'
        }
      });

      expect(res.status).toBe(400);
      const json = await res.json();
      expect(json.error).toContain('用户名已存在');
    });
  });
});
```

### 2. 测试Service与数据库集成
```typescript
describe('UserService与数据库集成', () => {
  let dataSource: DataSource;
  let userService: UserService;

  beforeEach(async () => {
    dataSource = await getTestDataSource();
    await dataSource.initialize();

    const userRepo = dataSource.getRepository(User);
    userService = new UserService(userRepo);
  });

  afterEach(async () => {
    await dataSource.destroy();
  });

  it('应该从数据库查询用户', async () => {
    // 创建测试数据
    const userRepo = dataSource.getRepository(User);
    const createdUser = await userRepo.save({
      username: 'testuser',
      email: 'test@example.com',
      password: 'hashedpassword'
    });

    // 测试查询
    const foundUser = await userService.findById(createdUser.id);

    expect(foundUser).toBeDefined();
    expect(foundUser?.username).toBe('testuser');
  });

  it('应该正确处理事务', async () => {
    await dataSource.transaction(async (manager) => {
      const user1 = await manager.save(User, {
        username: 'user1',
        email: 'user1@example.com',
        password: 'hash'
      });

      const user2 = await manager.save(User, {
        username: 'user2',
        email: 'user2@example.com',
        password: 'hash'
      });

      expect(user1.id).toBeDefined();
      expect(user2.id).toBeDefined();
    });
  });
});
```

### 3. 测试中间件集成
```typescript
import { authMiddleware } from '../src/middleware/auth.middleware';
import { generateToken } from '@d8d/shared-utils/jwt.util';

describe('认证中间件集成测试', () => {
  it('应该允许有效token的请求', async () => {
    const app = new Hono();
    app.use('/api/protected/*', authMiddleware);
    app.get('/api/protected/data', (c) => c.json({ message: 'Protected data' }));

    const token = generateToken({ userId: 1, username: 'testuser' });
    const res = await integrateRoutes(app).GET('/api/protected/data', {
      headers: {
        Authorization: `Bearer ${token}`
      }
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ message: 'Protected data' });
  });

  it('应该拒绝无效token的请求', async () => {
    const app = new Hono();
    app.use('/api/protected/*', authMiddleware);
    app.get('/api/protected/data', (c) => c.json({ message: 'Protected data' }));

    const res = await integrateRoutes(app).GET('/api/protected/data', {
      headers: {
        Authorization: 'Bearer invalid-token'
      }
    });

    expect(res.status).toBe(401);
  });
});
```

## 测试数据工厂模式

### factories.ts
```typescript
import { User } from '../src/entities/user.entity';
import { Repository } from 'typeorm';

export function buildUser(overrides = {}): Partial<User> {
  return {
    id: Math.floor(Math.random() * 10000),
    username: `testuser_${Date.now()}`,
    email: `test_${Date.now()}@example.com`,
    password: 'hashedpassword',
    role: 'user',
    active: true,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides
  };
}

export async function createTestUser(
  userRepo: Repository<User>,
  overrides = {}
): Promise<User> {
  const userData = buildUser(overrides);
  const user = userRepo.create(userData);
  return await userRepo.save(user);
}

// 批量创建测试数据
export async function createTestUsers(
  userRepo: Repository<User>,
  count: number,
  overrides = {}
): Promise<User[]> {
  const users = [];
  for (let i = 0; i < count; i++) {
    const user = await createTestUser(userRepo, {
      username: `user${i}`,
      email: `user${i}@example.com`,
      ...overrides
    });
    users.push(user);
  }
  return users;
}
```

## 数据库测试策略

### 1. 使用事务回滚
```typescript
describe('使用事务回滚', () => {
  let queryRunner: QueryRunner;
  let dataSource: DataSource;

  beforeEach(async () => {
    dataSource = await getTestDataSource();
    await dataSource.initialize();
    queryRunner = dataSource.createQueryRunner();
    await queryRunner.startTransaction();
  });

  afterEach(async () => {
    await queryRunner.rollbackTransaction();
    await queryRunner.release();
    await dataSource.destroy();
  });

  it('每个测试后自动回滚', async () => {
    const userRepo = queryRunner.manager.getRepository(User);
    await userRepo.save({
      username: 'testuser',
      email: 'test@example.com',
      password: 'hash'
    });

    // 数据存在于事务中
    const users = await userRepo.find();
    expect(users).toHaveLength(1);

    // 测试结束后自动回滚，数据库为空
  });
});
```

### 2. 使用测试数据库
```typescript
// tests/fixtures/test-db.ts
import { DataSource } from 'typeorm';
import { User } from '../src/entities/user.entity';
import { Role } from '../src/entities/role.entity';

export async function getTestDataSource(): Promise<DataSource> {
  return new DataSource({
    type: 'postgres',
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: 'test_password',
    database: 'test_d8dai', // 独立的测试数据库
    entities: [User, Role],
    synchronize: true, // 自动同步表结构
    dropSchema: true, // 每次测试前清空数据库
    logging: false // 测试环境关闭SQL日志
  });
}
```

## 覆盖率标准

| 测试类型 | 最低要求 | 目标要求 | 关键模块要求 |
|----------|----------|----------|--------------|
| 单元测试 | 70% | 80% | 90% |
| 集成测试 | 50% | 60% | 70% |

**关键模块定义**：
- 认证授权Service：90%单元测试覆盖率
- 数据库操作Service：85%单元测试覆盖率
- 核心业务逻辑：80%集成测试覆盖率

## 运行测试

### 本地开发
```bash
# 运行所有测试
cd packages/user-module && pnpm test

# 只运行单元测试
pnpm test:unit

# 只运行集成测试
pnpm test:integration

# 生成覆盖率报告
pnpm test:coverage

# 运行特定测试文件
pnpm test user.service.test.ts

# 运行特定测试用例
pnpm test --testNamePattern="应该创建用户"
```

### CI/CD
```yaml
user-module-tests:
  runs-on: ubuntu-latest
  services:
    postgres:
      image: postgres:17
      env:
        POSTGRES_PASSWORD: test_password
        POSTGRES_DB: test_d8dai
  steps:
    - run: cd packages/user-module && pnpm install
    - run: cd packages/user-module && pnpm test

all-modules-tests:
  runs-on: ubuntu-latest
  steps:
    - run: cd packages/user-module && pnpm test
    - run: cd packages/auth-module && pnpm test
    - run: cd packages/file-module && pnpm test
```

## 测试命名约定

### 文件命名
- Service单元测试：`[service].service.test.ts`
- Schema单元测试：`[schema].schema.test.ts`
- 路由集成测试：`[route].routes.integration.test.ts`
- Service集成测试：`[service].service.integration.test.ts`

### 测试描述
```typescript
describe('UserService', () => {
  describe('findById()', () => {
    it('应该返回用户数据', async () => { });
    it('应该返回null当用户不存在', async () => { });
  });

  describe('createUser()', () => {
    it('应该创建新用户', async () => { });
    it('应该拒绝重复的用户名', async () => { });
  });
});
```

## 常见错误避免

### ❌ 不要在单元测试中使用真实数据库
```typescript
// 错误：单元测试连接真实数据库
it('应该查询用户', async () => {
  const dataSource = await getTestDataSource(); // 不要在单元测试中
  // ...
});

// 正确：使用模拟的Repository
it('应该查询用户', async () => {
  const mockRepo = { findOne: vi.fn() };
  const userService = new UserService(mockRepo);
  // ...
});
```

### ❌ 不要在集成测试中mock数据库
```typescript
// 错误：集成测试中mock数据库
vi.mock('typeorm', () => ({
  getRepository: vi.fn(() => mockRepo)
}));

// 正确：使用真实数据库
const dataSource = await getTestDataSource();
const userRepo = dataSource.getRepository(User);
```

### ❌ 不要共享测试数据
```typescript
// 错误：测试间共享数据
let testUserId: number;

it('应该创建用户', async () => {
  const user = await createUser({ username: 'test' });
  testUserId = user.id; // 污染下一个测试
});

it('应该更新用户', async () => {
  await updateUser(testUserId, { username: 'updated' }); // 依赖上一个测试
});

// 正确：每个测试独立创建数据
it('应该更新用户', async () => {
  const user = await createTestUser(dataSource);
  await updateUser(user.id, { username: 'updated' });
});
```

## 调试技巧

### 1. 查看SQL查询
```typescript
const dataSource = new DataSource({
  // ...
  logging: true, // 显示所有SQL
  maxQueryExecutionTime: 100 // 记录慢查询
});
```

### 2. 打印测试数据
```typescript
it('调试测试', async () => {
  const users = await userService.findAll();
  console.debug('用户列表:', users);
  console.debug('用户数量:', users.length);
});
```

### 3. 使用only运行特定测试
```typescript
it.only('只运行这个测试', async () => {
  // ...
});
```

## 参考实现

- 用户模块包：`packages/user-module/tests/`
- 认证模块包：`packages/auth-module/tests/`
- 文件模块包：`packages/file-module/tests/`

## 相关文档

- [测试策略概述](./testing-strategy.md)
- [Web UI包测试规范](./web-ui-testing-standards.md)
- [Web Server包测试规范](./web-server-testing-standards.md)
- [Mini UI包测试规范](./mini-ui-testing-standards.md)
- [后端模块包开发规范](./backend-module-package-standards.md)

---

**文档状态**: 正式版
**适用范围**: packages/*-module
