# 智慧酒店 App 自动化测试方案

## 1. 测试策略总览

### 1.1 测试金字塔

```
         ┌──────────┐
         │  集成测试  │  ← 少量，覆盖核心流程（登录→首页→详情）
         ├──────────┤
         │ Widget测试│  ← 中等，每个页面至少 1 个冒烟测试
         ├──────────┤
         │  单元测试  │  ← 大量，Cubit/Service/Repository/BO 全覆盖
         └──────────┘
```

### 1.2 测试范围

| 测试类型 | 覆盖目标 | 工具 | 优先级 |
|---------|---------|------|--------|
| 单元测试 - BO | `fromJson` / `toJson` 正确性 | `flutter_test` | 高 |
| 单元测试 - Repository | API 调用正确、参数传递 | `flutter_test` + `mockito` | 高 |
| 单元测试 - Service | 业务逻辑正确、异常处理 | `flutter_test` + `mockito` | 高 |
| 单元测试 - Cubit | 状态流转正确 | `flutter_bloc_test` + `mockito` | 高 |
| Widget 测试 | 页面渲染、交互行为 | `flutter_test` | 中 |
| 集成测试 | 端到端流程 | `integration_test` | 低 |

### 1.3 依赖配置

在 `pubspec.yaml` 的 `dev_dependencies` 中添加：

```yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  bloc_test: ^9.1.7
  mockito: ^5.4.4
  build_runner: ^2.4.8
  integration_test:
    sdk: flutter
```

运行 `flutter pub get` 安装。

---

## 2. 测试目录结构

```
test/
├── unit/                          # 单元测试
│   ├── models/                    # BO 模型测试
│   │   ├── alarm_bo_test.dart
│   │   ├── device_bo_test.dart
│   │   ├── inspection_bo_test.dart
│   │   ├── topology_bo_test.dart
│   │   ├── energy_bo_test.dart
│   │   ├── room_bo_test.dart
│   │   └── rule_bo_test.dart
│   ├── repositories/              # Repository 测试
│   │   ├── alarm_repository_test.dart
│   │   ├── device_repository_test.dart
│   │   ├── inspection_repository_test.dart
│   │   ├── service_repository_test.dart
│   │   └── report_repository_test.dart
│   ├── services/                  # Service 测试
│   │   ├── alarm_service_test.dart
│   │   ├── device_service_test.dart
│   │   ├── inspection_service_test.dart
│   │   ├── service_room_service_test.dart
│   │   └── report_service_test.dart
│   └── cubits/                    # Cubit 测试
│       ├── home_index_cubit_test.dart
│       ├── abnormal_list_cubit_test.dart
│       ├── abnormal_cubit_test.dart
│       ├── device_cubit_test.dart
│       ├── inspection_cubit_test.dart
│       ├── inspection_device_cubit_test.dart
│       ├── inspection_history_cubit_test.dart
│       ├── inspection_topology_cubit_test.dart
│       ├── service_index_cubit_test.dart
│       ├── service_room_cubit_test.dart
│       ├── service_device_cubit_test.dart
│       ├── report_index_cubit_test.dart
│       ├── device_list_cubit_test.dart
│       ├── energy_cubit_test.dart
│       ├── room_report_cubit_test.dart
│       ├── rule_management_cubit_test.dart
│       └── profile_cubit_test.dart
├── widget/                        # Widget 测试
│   ├── login_view_test.dart
│   ├── home_index_view_test.dart
│   ├── abnormal_list_view_test.dart
│   └── ...                        # 每个页面一个
└── integration/                   # 集成测试
    └── app_test.dart
```

---

## 3. 单元测试规范

### 3.1 BO 模型测试

**目的**：验证 JSON 反序列化正确性

```dart
// test/unit/models/alarm_bo_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:smart_hotel_app/models/bo/alarm_bo.dart';

void main() {
  group('AlarmItemBO', () {
    test('fromJson should parse correctly', () {
      final json = {
        'id': '1',
        'title': '温度异常告警',
        'deviceInfo': 'A-301智能空开',
        'alarmTime': '2026-05-09 14:23:18',
        'processTime': '2026-05-09 15:00:00',
        'status': 'pending',
        'level': '高警',
        'urgency': '紧急',
      };

      final bo = AlarmItemBO.fromJson(json);

      expect(bo.id, '1');
      expect(bo.title, '温度异常告警');
      expect(bo.deviceInfo, 'A-301智能空开');
      expect(bo.status, 'pending');
      expect(bo.level, '高警');
    });

    test('fromJson should handle missing fields', () {
      final json = <String, dynamic>{};

      final bo = AlarmItemBO.fromJson(json);

      expect(bo.id, '');
      expect(bo.title, '');
      expect(bo.status, 'pending'); // 默认值
    });

    test('fromJson should handle null list fields', () {
      final json = {
        'temperatureSpots': null,
        'alarmType': 'test',
        'deviceInfo': 'test',
        // ... 其他必填字段
      };

      // 验证不抛异常
      expect(() => AlarmDetailBO.fromJson(json), returnsNormally);
    });
  });
}
```

**AI 可自动化**：是。给定 JSON 示例和 BO 定义，AI 可自动生成所有字段的断言。

### 3.2 Repository 测试

**目的**：验证 API 调用参数正确、fromJsonT 回调正确

```dart
// test/unit/repositories/alarm_repository_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:dio/dio.dart';
import 'package:smart_hotel_app/http/dio_request.dart';
import 'package:smart_hotel_app/http/response_model.dart';
import 'package:smart_hotel_app/repositories/alarm_repository.dart';
import 'package:smart_hotel_app/models/bo/alarm_bo.dart';

// 生成 Mock 类
// 运行: flutter pub run build_runner build
@GenerateMocks([DioRequest])
import 'alarm_repository_test.mocks.dart';

void main() {
  late MockDioRequest mockDio;
  late AlarmRepository repository;

  setUp(() {
    mockDio = MockDioRequest();
    repository = AlarmRepository();
  });

  group('getAlarms', () {
    test('should return list of AlarmItemBO on success', () async {
      // 注意：由于 DioRequest 是单例，实际测试时需要重构为可注入
      // 或者使用 mock HTTP 拦截器
      // 见下文 7.1 节
    });
  });
}
```

> **重要**：当前 `DioRequest` 是单例模式，Repository 直接调用 `DioRequest.instance`，无法 mock。建议在对接时同时将 Repository 改为接收 `DioRequest` 实例（依赖注入），或者使用 Dio 的 `HttpClientAdapter` 进行 mock。

### 3.3 Service 测试

**目的**：验证业务逻辑正确、异常处理

```dart
// test/unit/services/alarm_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:smart_hotel_app/services/alarm_service.dart';
import 'package:smart_hotel_app/repositories/alarm_repository.dart';
import 'package:smart_hotel_app/models/bo/alarm_bo.dart';
import 'package:smart_hotel_app/http/response_model.dart';

@GenerateMocks([AlarmRepository])
import 'alarm_service_test.mocks.dart';

void main() {
  late MockAlarmRepository mockRepository;
  late AlarmService service;

  setUp(() {
    mockRepository = MockAlarmRepository();
    service = AlarmService(repository: mockRepository);
  });

  group('getAlarms', () {
    final mockAlarms = [
      AlarmItemBO(
        id: '1', title: '告警1', deviceInfo: '设备A',
        alarmTime: '', processTime: '', status: 'pending',
        level: '高警', urgency: '紧急',
      ),
    ];

    test('should return alarms on success', () async {
      when(mockRepository.getAlarms()).thenAnswer(
        (_) async => ResponseModel<List<AlarmItemBO>>(
          code: 200, msg: 'success', data: mockAlarms,
          uuid: 'test', success: true, timestamp: 0,
        ),
      );

      final result = await service.getAlarms();

      expect(result, mockAlarms);
      verify(mockRepository.getAlarms()).called(1);
    });

    test('should throw on failure', () async {
      when(mockRepository.getAlarms()).thenAnswer(
        (_) async => ResponseModel<List<AlarmItemBO>>(
          code: 500, msg: '服务器错误', data: null,
          uuid: 'test', success: false, timestamp: 0,
        ),
      );

      expect(() => service.getAlarms(), throwsException);
    });
  });
}
```

**AI 可自动化**：是。Service 测试模式固定，AI 可按模板批量生成。

### 3.4 Cubit 测试（使用 bloc_test）

**目的**：验证状态流转正确

```dart
// test/unit/cubits/home_index_cubit_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:smart_hotel_app/views/home/index/cubit/home_index_cubit.dart';
import 'package:smart_hotel_app/views/home/index/cubit/home_index_state.dart';
import 'package:smart_hotel_app/services/home_service.dart';

@GenerateMocks([HomeService])
import 'home_index_cubit_test.mocks.dart';

void main() {
  late MockHomeService mockService;

  setUp(() {
    mockService = MockHomeService();
  });

  group('HomeIndexCubit', () {
    blocTest<HomeIndexCubit, HomeIndexState>(
      'should emit [loading, loaded] when loadData succeeds',
      build: () => HomeIndexCubit(service: mockService),
      act: (cubit) {
        // loadData() 在构造函数中自动调用
        // 或手动调用 cubit.loadData()
      },
      expect: () => [
        isA<HomeIndexState>().having((s) => s.isLoading, 'isLoading', true),
        isA<HomeIndexState>()
            .having((s) => s.isLoading, 'isLoading', false)
            .having((s) => s.highAlarmCount, 'highAlarmCount', isPositive),
      ],
    );

    blocTest<HomeIndexCubit, HomeIndexState>(
      'should emit [loading, error] when loadData fails',
      build: () {
        when(mockService.getOverview()).thenThrow(Exception('网络错误'));
        return HomeIndexCubit(service: mockService);
      },
      expect: () => [
        isA<HomeIndexState>().having((s) => s.isLoading, 'isLoading', true),
        isA<HomeIndexState>()
            .having((s) => s.isLoading, 'isLoading', false)
            .having((s) => s.error, 'error', isNotNull),
      ],
    );
  });
}
```

**AI 可自动化**：是。每个 Cubit 的测试模式相同（loading → success/error），AI 可批量生成。

---

## 4. Widget 测试规范

### 4.1 冒烟测试（每个页面至少 1 个）

**目的**：验证页面能正常渲染，不抛异常

```dart
// test/widget/home_index_view_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/views/home/index/index_view.dart';
import 'package:smart_hotel_app/views/home/index/cubit/home_index_cubit.dart';
import 'package:smart_hotel_app/views/home/index/cubit/home_index_state.dart';

void main() {
  testWidgets('HomeView should render without error', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: BlocProvider<HomeIndexCubit>(
          create: (_) => HomeIndexCubit(),
          child: const HomeView(),
        ),
      ),
    );

    // 等待初始化完成
    await tester.pumpAndSettle();

    // 验证页面关键元素存在
    expect(find.byType(HomeView), findsOneWidget);
  });
}
```

**AI 可自动化**：是。每个页面一个冒烟测试，模式固定。

### 4.2 交互测试（关键页面）

```dart
testWidgets('tapping alarm item should navigate to detail', (tester) async {
  // 需要 mock 路由
  await tester.pumpWidget(
    MaterialApp(
      home: BlocProvider<AbnormalListCubit>(
        create: (_) => AbnormalListCubit(service: mockService),
        child: const AbnormalListView(),
      ),
    ),
  );

  await tester.pumpAndSettle();
  await tester.tap(find.text('温度异常告警'));
  await tester.pumpAndSettle();

  // 验证导航到详情页
  expect(find.byType(AbnormalDetailView), findsOneWidget);
});
```

**AI 可自动化**：需要人工提供交互路径描述，AI 可生成测试代码。

---

## 5. 集成测试

### 5.1 核心流程测试

```dart
// test/integration/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('full login flow', (tester) async {
    // 启动 App
    await tester.pumpWidget(const MyApp());
    await tester.pumpAndSettle();

    // 验证在登录页
    expect(find.text('登录'), findsOneWidget);

    // 输入用户名密码
    await tester.enterText(find.byType(TextFormField).first, 'admin');
    await tester.enterText(find.byType(TextFormField).last, '123456');
    await tester.tap(find.text('登录'));
    await tester.pumpAndSettle();

    // 验证跳转到首页
    expect(find.text('告警'), findsOneWidget);
  });
}
```

**AI 可自动化**：需要人工描述流程步骤，AI 可生成代码。**但集成测试需要在真机/模拟器上运行，AI 无法自动执行验证。**

---

## 6. AI 自动化能力评估

### 6.1 AI 可完全自动化

| 测试类型 | 自动化程度 | 说明 |
|---------|-----------|------|
| BO 模型 `fromJson` 测试 | 100% | 给定 JSON 样本和 BO 定义，AI 可生成全字段断言 |
| BO 模型 `null` 安全测试 | 100% | 验证缺失字段/空列表不抛异常 |
| Service 测试（成功/失败） | 100% | 模式固定，mock Repository 返回成功/失败 |
| Cubit 状态流转测试 | 100% | 使用 `blocTest`，模式固定 |
| Widget 冒烟测试 | 100% | 每个页面验证能渲染，模式固定 |

### 6.2 AI 需要人工辅助

| 测试类型 | 需人工介入 | 原因 |
|---------|-----------|------|
| Repository 测试 | 需要 mock DioRequest 方案 | 当前 DioRequest 是单例，需先改造为可注入 |
| Widget 交互测试 | 需要描述交互路径 | AI 不知道哪个按钮做什么操作 |
| 集成测试 | 需要描述完整流程 | AI 不知道业务操作步骤 |
| 测试验证 | 需要人工执行 | AI 无法在真机上运行测试 |

### 6.3 需要人工执行的操作

| 操作 | 原因 |
|------|------|
| `flutter test` 运行 | AI 无法执行终端命令并观察结果 |
| `flutter run` 真机验证 | 需要在真机上观察 UI 渲染 |
| 测试失败后的排查 | 需要分析失败原因，可能是测试代码问题或业务代码 bug |
| 覆盖率报告分析 | 需要判断哪些分支未覆盖 |
| mock 数据维护 | 接口变更时需要更新 mock JSON |

---

## 7. 关键注意事项

### 7.1 DioRequest 单例导致 Repository 不可测试

**问题**：`DioRequest` 是单例，Repository 直接调用 `DioRequest.instance`，无法注入 mock。

**解决方案**（在对按时一并改造）：

```dart
// 方案 A：Repository 接收 DioRequest 实例（推荐）
class AlarmRepository {
  final DioRequest _dio;
  AlarmRepository({DioRequest? dio}) : _dio = dio ?? DioRequest.instance;

  Future<ResponseModel<List<AlarmItemBO>>> getAlarms() {
    return _dio.get<List<AlarmItemBO>>('/api/alarms', ...);
  }
}

// 方案 B：使用 Dio HttpClientAdapter mock
// 通过 Dio.httpClientAdapter 注入 mock HTTP 客户端
```

**建议在对按时统一采用方案 A**，确保所有 Repository 可测试。

### 7.2 State 缺少 isLoading/error 字段

部分页面 State 没有 `isLoading` 和 `error` 字段（如 `AbnormalState`、`DeviceState`），需要在对按时一并添加：

```dart
class AbnormalState extends Equatable {
  final AlarmDetailBO? alarmInfo;
  final bool isLoading;
  final String? error;

  // ... copyWith, props
}
```

### 7.3 测试执行命令

```bash
# 运行所有单元测试
flutter test test/unit/

# 运行指定模块测试
flutter test test/unit/cubits/

# 运行 widget 测试
flutter test test/widget/

# 运行集成测试
flutter test integration_test/

# 生成覆盖率报告
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html

# 生成 mock 类
flutter pub run build_runner build --delete-conflicting-outputs
```

---

## 8. 测试执行建议

### 8.1 分阶段执行

```
阶段 1：BO 模型测试（对接前）
  - 创建 BO 文件后立即运行，验证 fromJson 正确
  - 不需要 mock，可独立运行

阶段 2：Service + Repository 测试（对接中）
  - 创建 Service/Repository 后运行
  - 需要 mock Repository

阶段 3：Cubit 测试（对接中）
  - 改造 Cubit 后运行
  - 需要 mock Service

阶段 4：Widget 冒烟测试（对接后）
  - 每个页面改造完成后运行
  - 验证 UI 仍能正常渲染

阶段 5：集成测试（联调后）
  - 在真机/模拟器上运行
  - 需要后端服务可用
```

### 8.2 CI/CD 集成

```yaml
# .github/workflows/test.yml 示例
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - run: flutter pub get
      - run: flutter test --coverage
      - run: flutter test integration_test/
```

---

## 9. AI 批量生成测试代码提示词模板

如果让 AI 批量生成测试，可以使用以下提示词模板：

```
请根据以下测试方案，为 [模块名] 模块生成完整的测试代码：

1. BO 模型测试：[BO文件名]，JSON 示例为 [JSON示例]
2. Service 测试：[Service类名]，方法返回 [BO类型]
3. Cubit 测试：[Cubit类名]，状态包含 [State字段列表]
4. Widget 冒烟测试：[View类名]

要求：
- 使用 mockito 生成 mock 类
- 覆盖成功和失败两种情况
- 每个测试文件包含完整 import
- 遵循现有的测试命名规范
```

---

## 10. 总结

| 环节 | AI 可自动化 | 需人工 | 备注 |
|------|:----------:|:-----:|------|
| 生成测试代码 | 是 | - | 按模板批量生成 |
| 运行测试 | - | 是 | 终端执行 `flutter test` |
| 分析测试结果 | - | 是 | 判断失败原因 |
| 修复测试失败 | 是（简单） | 是（复杂） | 简单断言错误 AI 可修 |
| 更新 mock 数据 | 是 | 是 | 接口变更时同步更新 |
| 真机验证 | - | 是 | 必须在真机上运行 |
| 覆盖率报告 | - | 是 | 人工判断哪些分支需补测 |