---
description: 桌面端测试
globs:
alwaysApply: false
---
# 桌面端控制器单元测试指南

## 测试框架与目录结构

LobeChat 桌面端使用 Vitest 作为测试框架。控制器的单元测试应放置在对应控制器文件同级的 `__tests__` 目录下，并以原控制器文件名加 `.test.ts` 作为文件名。

```
apps/desktop/src/main/controllers/
├── __tests__/
│   ├── index.test.ts
│   ├── MenuCtr.test.ts
│   └── ...
├── McpCtr.ts
├── MenuCtr.ts
└── ...
```

## 测试文件基本结构

```typescript
import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { App } from '@/core/App';

import YourController from '../YourControllerName';

// 模拟依赖
vi.mock('依赖模块', () => ({
  依赖函数: vi.fn(),
}));

// 模拟 App 实例
const mockApp = {
  // 按需模拟必要的 App 属性和方法
} as unknown as App;

describe('YourController', () => {
  let controller: YourController;

  beforeEach(() => {
    vi.clearAllMocks();
    controller = new YourController(mockApp);
  });

  describe('方法名', () => {
    it('测试场景描述', async () => {
      // 准备测试数据

      // 执行被测方法
      const result = await controller.方法名(参数);

      // 验证结果
      expect(result).toMatchObject(预期结果);
    });
  });
});
```

## 模拟外部依赖

### 模拟模块函数

```typescript
const mockFunction = vi.fn();

vi.mock('module-name', () => ({
  functionName: mockFunction,
}));
```

### 模拟 Node.js 核心模块

例如模拟 `child_process.exec` 和 `util.promisify`:

```typescript
// 存储模拟的 exec 实现
const mockExecImpl = vi.fn();

// 模拟 child_process.exec
vi.mock('child_process', () => ({
  exec: vi.fn((cmd, callback) => {
    return mockExecImpl(cmd, callback);
  }),
}));

// 模拟 util.promisify
vi.mock('util', () => ({
  promisify: vi.fn((fn) => {
    return async (cmd: string) => {
      return new Promise((resolve, reject) => {
        mockExecImpl(cmd, (error: Error | null, result: any) => {
          if (error) reject(error);
          else resolve(result);
        });
      });
    };
  }),
}));
```

## 编写有效的测试用例

### 测试分类

将测试用例分为不同类别，每个类别测试一个特定场景：

```typescript
// 成功场景
it('应该成功完成操作', async () => {});

// 边界条件
it('应该处理边界情况', async () => {});

// 错误处理
it('应该优雅地处理错误', async () => {});
```

### 设置测试数据

```typescript
// 模拟返回值
mockExecImpl.mockImplementation((cmd: string, callback: any) => {
  if (cmd === '命令') {
    callback(null, { stdout: '成功输出' });
  } else {
    callback(new Error('错误信息'), null);
  }
});
```

### 断言

使用 Vitest 的断言函数验证结果：

```typescript
// 检查基本值
expect(result.success).toBe(true);

// 检查对象部分匹配
expect(result.data).toMatchObject({
  key: 'value',
});

// 检查数组
expect(result.items).toHaveLength(2);
expect(result.items[0].name).toBe('expectedName');

// 检查函数调用
expect(mockFunction).toHaveBeenCalledWith(expectedArgs);
expect(mockFunction).toHaveBeenCalledTimes(1);
```

## 最佳实践

1. **隔离测试**：确保每个测试互不影响，使用 `beforeEach` 重置模拟和状态
2. **全面覆盖**：测试正常流程、边界条件和错误处理
3. **清晰命名**：测试名称应清晰描述测试内容和预期结果
4. **避免测试实现细节**：测试应该关注行为而非实现细节，使代码重构不会破坏测试
5. **模拟外部依赖**：使用 `vi.mock()` 模拟所有外部依赖，减少测试的不确定性

## 示例：测试 IPC 事件处理方法

```typescript
it('应该正确处理 IPC 事件', async () => {
  // 模拟依赖
  mockSomething.mockReturnValue({ result: 'success' });

  // 调用 IPC 方法
  const result = await controller.ipcMethodName({
    param1: 'value1',
    param2: 'value2',
  });

  // 验证结果
  expect(result).toEqual({
    success: true,
    data: { result: 'success' },
  });

  // 验证依赖调用
  expect(mockSomething).toHaveBeenCalledWith('value1', 'value2');
});
```
