1097 lines
25 KiB
Markdown
1097 lines
25 KiB
Markdown
# 测试指南
|
||
|
||
> 本文档提供 APK 升级模块、遥测模块的手动测试和自动化测试方法。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [测试环境准备](#1-测试环境准备)
|
||
2. [APK 升级模块测试](#2-apk-升级模块测试)
|
||
3. [遥测模块测试](#3-遥测模块测试)
|
||
4. [自动化单元测试](#4-自动化单元测试)
|
||
5. [Mock 服务器搭建](#5-mock-服务器搭建)
|
||
|
||
---
|
||
|
||
## 1. 测试环境准备
|
||
|
||
### 1.1 开发环境
|
||
|
||
```bash
|
||
# 确保 Flutter 环境正常
|
||
flutter doctor
|
||
|
||
# 获取依赖
|
||
cd c:\Users\dong\Desktop\rwadurian\frontend\mobile-app
|
||
flutter pub get
|
||
```
|
||
|
||
### 1.2 测试设备
|
||
|
||
| 测试项 | 推荐设备 |
|
||
|-------|---------|
|
||
| APK 安装 | Android 真机 (API 24+) |
|
||
| 遥测功能 | Android 真机或模拟器 |
|
||
| 心跳服务 | Android 真机 (测试前后台切换) |
|
||
|
||
### 1.3 构建测试 APK
|
||
|
||
```bash
|
||
# Debug 版本 (用于开发测试)
|
||
flutter build apk --debug
|
||
|
||
# Release 版本 (用于正式测试)
|
||
flutter build apk --release
|
||
|
||
# 输出路径
|
||
# build/app/outputs/flutter-apk/app-debug.apk
|
||
# build/app/outputs/flutter-apk/app-release.apk
|
||
```
|
||
|
||
---
|
||
|
||
## 2. APK 升级模块测试
|
||
|
||
### 2.1 手动测试清单
|
||
|
||
#### 测试用例 1: 版本检测 - 有新版本
|
||
|
||
**前置条件**:
|
||
- 当前安装版本: 1.0.0 (versionCode: 1)
|
||
- 服务器配置新版本: 1.1.0 (versionCode: 2)
|
||
|
||
**测试步骤**:
|
||
1. 启动应用,进入主页面(龙虎榜)
|
||
2. 等待 3 秒后观察是否弹出更新对话框
|
||
|
||
**预期结果**:
|
||
- 弹出更新对话框
|
||
- 显示新版本号 "1.1.0"
|
||
- 显示文件大小
|
||
- 显示更新日志(如有)
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 Checking for update...
|
||
flutter: 📊 New version available: 1.1.0
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 2: 版本检测 - 已是最新版本
|
||
|
||
**前置条件**:
|
||
- 当前安装版本与服务器最新版本一致
|
||
|
||
**测试步骤**:
|
||
1. 启动应用,进入主页面
|
||
2. 等待 3 秒
|
||
|
||
**预期结果**:
|
||
- 不弹出更新对话框
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: Already latest version
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 3: 强制更新
|
||
|
||
**前置条件**:
|
||
- 服务器配置 `forceUpdate: true`
|
||
|
||
**测试步骤**:
|
||
1. 启动应用,触发版本检测
|
||
2. 尝试点击对话框外部关闭
|
||
3. 尝试按返回键关闭
|
||
|
||
**预期结果**:
|
||
- 对话框标题显示"发现重要更新"
|
||
- 无法通过点击外部或返回键关闭对话框
|
||
- 没有"稍后"按钮
|
||
|
||
---
|
||
|
||
#### 测试用例 4: APK 下载与安装
|
||
|
||
**前置条件**:
|
||
- 服务器已配置有效的 APK 下载链接
|
||
- APK 已正确签名
|
||
|
||
**测试步骤**:
|
||
1. 点击"立即更新"按钮
|
||
2. 观察下载进度
|
||
3. 下载完成后观察安装流程
|
||
|
||
**预期结果**:
|
||
- 显示下载进度对话框
|
||
- 进度百分比实时更新
|
||
- 下载完成后自动弹出系统安装界面
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📥 Downloading APK from: https://...
|
||
flutter: 📥 Download progress: 50%
|
||
flutter: ✅ APK downloaded successfully
|
||
flutter: 🔐 SHA-256 verification passed
|
||
flutter: 📲 Installing APK...
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 5: 下载取消
|
||
|
||
**测试步骤**:
|
||
1. 开始下载更新
|
||
2. 在下载过程中点击"取消"按钮
|
||
|
||
**预期结果**:
|
||
- 下载立即停止
|
||
- 显示"下载已取消"
|
||
- 临时文件被清理
|
||
|
||
---
|
||
|
||
#### 测试用例 6: 下载失败重试
|
||
|
||
**前置条件**:
|
||
- 模拟网络不稳定或服务器错误
|
||
|
||
**测试步骤**:
|
||
1. 开始下载更新
|
||
2. 断开网络或服务器返回错误
|
||
3. 观察错误提示
|
||
4. 点击"重试"按钮
|
||
|
||
**预期结果**:
|
||
- 显示"下载失败,请稍后重试"
|
||
- 出现"重试"按钮
|
||
- 点击重试后重新开始下载
|
||
|
||
---
|
||
|
||
#### 测试用例 7: SHA-256 校验失败
|
||
|
||
**前置条件**:
|
||
- 服务器返回的 SHA-256 与实际文件不匹配
|
||
|
||
**测试步骤**:
|
||
1. 下载 APK
|
||
2. 等待校验完成
|
||
|
||
**预期结果**:
|
||
- 显示下载失败
|
||
- 日志显示校验失败信息
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: ❌ SHA-256 verification failed
|
||
flutter: Expected: abc123...
|
||
flutter: Actual: def456...
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 8: 应用市场来源检测
|
||
|
||
**前置条件**:
|
||
- 从 Google Play 安装的应用
|
||
|
||
**测试步骤**:
|
||
1. 启动应用
|
||
2. 触发版本检测
|
||
|
||
**预期结果**:
|
||
- 显示"检测到您的应用来自应用市场"提示
|
||
- 按钮变为"前往应用市场"
|
||
|
||
---
|
||
|
||
### 2.2 调试技巧
|
||
|
||
#### 强制触发版本检测
|
||
|
||
在代码中临时修改版本号进行测试:
|
||
|
||
```dart
|
||
// lib/core/updater/version_checker.dart
|
||
// 临时修改 checkForUpdate() 方法,强制返回测试数据
|
||
|
||
Future<VersionInfo?> checkForUpdate() async {
|
||
// 测试用: 返回模拟的新版本信息
|
||
return VersionInfo(
|
||
version: '1.1.0',
|
||
versionCode: 999,
|
||
downloadUrl: 'https://your-test-server.com/test.apk',
|
||
fileSize: 52428800,
|
||
fileSizeFriendly: '50.0 MB',
|
||
sha256: 'your-test-sha256-hash',
|
||
forceUpdate: false,
|
||
updateLog: '测试更新日志',
|
||
releaseDate: DateTime.now(),
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 使用 Charles/Fiddler 抓包
|
||
|
||
1. 配置手机代理到电脑
|
||
2. 监控 `/api/app/version/check` 请求
|
||
3. 可修改响应数据测试不同场景
|
||
|
||
---
|
||
|
||
## 3. 遥测模块测试
|
||
|
||
### 3.1 手动测试清单
|
||
|
||
#### 测试用例 1: 设备信息采集
|
||
|
||
**测试步骤**:
|
||
1. 首次启动应用
|
||
2. 等待 Splash 页面完成
|
||
|
||
**预期结果**:
|
||
- 控制台输出设备信息采集日志
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 Collecting device info...
|
||
flutter: 📊 Device: Samsung SM-G991B
|
||
flutter: 📊 OS: Android 13 (SDK 33)
|
||
flutter: 📊 App: 1.0.0 (1)
|
||
flutter: 📊 Screen: 1080x2400 @3.0x
|
||
flutter: 📊 Network: wifi
|
||
flutter: 📊 Locale: zh_CN
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 2: 会话开始事件
|
||
|
||
**测试步骤**:
|
||
1. 完全关闭应用
|
||
2. 重新启动应用
|
||
|
||
**预期结果**:
|
||
- 生成新的 sessionId
|
||
- 触发 `session_start` 事件
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 New session started: session-abc123
|
||
flutter: 📊 Event tracked: session_start
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 3: 会话结束事件
|
||
|
||
**测试步骤**:
|
||
1. 应用在前台运行
|
||
2. 按 Home 键将应用切到后台
|
||
|
||
**预期结果**:
|
||
- 触发 `session_end` 事件
|
||
- 事件包含会话时长
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 Session paused, duration: 120 seconds
|
||
flutter: 📊 Event tracked: session_end
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 4: 会话恢复 (30分钟内)
|
||
|
||
**测试步骤**:
|
||
1. 将应用切到后台
|
||
2. 5 分钟内切回前台
|
||
|
||
**预期结果**:
|
||
- 恢复原有 session
|
||
- 触发 `session_resume` 事件
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 Session resumed: session-abc123
|
||
flutter: 📊 Event tracked: session_resume
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 5: 新会话 (超过30分钟)
|
||
|
||
**测试步骤**:
|
||
1. 将应用切到后台
|
||
2. 等待超过 30 分钟(或修改超时配置为较短时间测试)
|
||
3. 切回前台
|
||
|
||
**预期结果**:
|
||
- 生成新的 sessionId
|
||
- 触发 `session_start` 事件(非 resume)
|
||
|
||
---
|
||
|
||
#### 测试用例 6: 心跳发送
|
||
|
||
**测试步骤**:
|
||
1. 保持应用在前台
|
||
2. 观察 60 秒内的日志
|
||
|
||
**预期结果**:
|
||
- 每 60 秒发送一次心跳
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 💓 Heartbeat sent
|
||
flutter: 💓 Heartbeat sent
|
||
...
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 7: 心跳暂停 (后台)
|
||
|
||
**测试步骤**:
|
||
1. 应用在前台,心跳正常发送
|
||
2. 将应用切到后台
|
||
3. 等待 2 分钟
|
||
4. 切回前台
|
||
|
||
**预期结果**:
|
||
- 后台期间不发送心跳
|
||
- 切回前台后立即发送心跳并恢复定时器
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 💓 Heartbeat paused (app in background)
|
||
... (后台期间无心跳日志)
|
||
flutter: 💓 Heartbeat resumed (app in foreground)
|
||
flutter: 💓 Heartbeat sent
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 8: 事件批量上传
|
||
|
||
**测试步骤**:
|
||
1. 在应用中执行多个操作(页面切换、按钮点击等)
|
||
2. 等待 30 秒或事件队列达到 10 条
|
||
|
||
**预期结果**:
|
||
- 触发批量上传
|
||
- 上传成功后清理本地队列
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 Queue size: 10, triggering upload
|
||
flutter: 📊 Uploading 10 events...
|
||
flutter: ✅ Uploaded 10 telemetry events
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 9: 上传失败重试
|
||
|
||
**前置条件**:
|
||
- 模拟网络断开或服务器错误
|
||
|
||
**测试步骤**:
|
||
1. 积累一些事件
|
||
2. 断开网络
|
||
3. 等待上传触发
|
||
|
||
**预期结果**:
|
||
- 上传失败但事件保留在本地队列
|
||
- 网络恢复后下次触发时重新上传
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: ❌ Upload error (DioException): Connection refused
|
||
flutter: 📊 Events kept in queue for retry
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 10: 远程配置同步
|
||
|
||
**测试步骤**:
|
||
1. 启动应用
|
||
2. 修改服务器配置(如 `samplingRate: 0.5`)
|
||
3. 等待配置同步周期(默认 1 小时,测试时可改短)
|
||
|
||
**预期结果**:
|
||
- 获取并应用新配置
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 Fetching telemetry config...
|
||
flutter: 📊 Config updated: samplingRate=0.5
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 11: 采样率过滤
|
||
|
||
**前置条件**:
|
||
- 配置 `samplingRate: 0.0`
|
||
|
||
**测试步骤**:
|
||
1. 应用新配置
|
||
2. 尝试触发事件
|
||
|
||
**预期结果**:
|
||
- 所有事件被过滤,不进入队列
|
||
|
||
**验证日志**:
|
||
```
|
||
flutter: 📊 Event filtered by sampling rate
|
||
```
|
||
|
||
---
|
||
|
||
#### 测试用例 12: 事件类型过滤
|
||
|
||
**前置条件**:
|
||
- 配置 `enabledEventTypes: ['error', 'crash']`
|
||
|
||
**测试步骤**:
|
||
1. 触发 `page_view` 事件
|
||
2. 触发 `error` 事件
|
||
|
||
**预期结果**:
|
||
- `page_view` 被过滤
|
||
- `error` 被记录
|
||
|
||
---
|
||
|
||
### 3.2 调试技巧
|
||
|
||
#### 查看本地事件队列
|
||
|
||
```dart
|
||
// 临时添加调试代码
|
||
final storage = TelemetryStorage();
|
||
final events = storage.peekEvents(100);
|
||
for (var e in events) {
|
||
debugPrint('Event: ${e.name} - ${e.type} - ${e.timestamp}');
|
||
}
|
||
```
|
||
|
||
#### 强制触发上传
|
||
|
||
```dart
|
||
// 在需要的地方调用
|
||
await TelemetryService().forceUpload();
|
||
```
|
||
|
||
#### 缩短配置同步周期
|
||
|
||
```dart
|
||
// lib/bootstrap.dart
|
||
await TelemetryService().initialize(
|
||
apiBaseUrl: _apiBaseUrl,
|
||
context: context,
|
||
configSyncInterval: const Duration(minutes: 1), // 测试用
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 自动化单元测试
|
||
|
||
### 4.1 测试文件结构
|
||
|
||
```
|
||
test/
|
||
├── unit/
|
||
│ ├── core/
|
||
│ │ ├── updater/
|
||
│ │ │ ├── version_checker_test.dart
|
||
│ │ │ ├── download_manager_test.dart
|
||
│ │ │ └── update_service_test.dart
|
||
│ │ └── telemetry/
|
||
│ │ ├── device_info_collector_test.dart
|
||
│ │ ├── telemetry_storage_test.dart
|
||
│ │ ├── telemetry_uploader_test.dart
|
||
│ │ ├── session_manager_test.dart
|
||
│ │ └── heartbeat_service_test.dart
|
||
│ └── mocks/
|
||
│ ├── mock_dio.dart
|
||
│ └── mock_storage.dart
|
||
```
|
||
|
||
### 4.2 VersionChecker 单元测试
|
||
|
||
```dart
|
||
// test/unit/core/updater/version_checker_test.dart
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:mocktail/mocktail.dart';
|
||
import 'package:dio/dio.dart';
|
||
import 'package:rwa_android_app/core/updater/version_checker.dart';
|
||
import 'package:rwa_android_app/core/updater/models/version_info.dart';
|
||
|
||
class MockDio extends Mock implements Dio {}
|
||
|
||
void main() {
|
||
late VersionChecker versionChecker;
|
||
late MockDio mockDio;
|
||
|
||
setUp(() {
|
||
mockDio = MockDio();
|
||
versionChecker = VersionChecker(
|
||
apiBaseUrl: 'https://api.test.com',
|
||
dio: mockDio, // 需要修改 VersionChecker 支持注入 Dio
|
||
);
|
||
});
|
||
|
||
group('VersionChecker', () {
|
||
test('should return VersionInfo when new version available', () async {
|
||
// Arrange
|
||
when(() => mockDio.get(
|
||
any(),
|
||
queryParameters: any(named: 'queryParameters'),
|
||
)).thenAnswer((_) async => Response(
|
||
data: {
|
||
'code': 0,
|
||
'data': {
|
||
'hasUpdate': true,
|
||
'version': '1.1.0',
|
||
'versionCode': 2,
|
||
'downloadUrl': 'https://cdn.test.com/app.apk',
|
||
'fileSize': 52428800,
|
||
'fileSizeFriendly': '50.0 MB',
|
||
'sha256': 'abc123',
|
||
'forceUpdate': false,
|
||
'updateLog': 'Bug fixes',
|
||
'releaseDate': '2024-01-15T10:00:00Z',
|
||
}
|
||
},
|
||
statusCode: 200,
|
||
requestOptions: RequestOptions(path: ''),
|
||
));
|
||
|
||
// Act
|
||
final result = await versionChecker.checkForUpdate();
|
||
|
||
// Assert
|
||
expect(result, isNotNull);
|
||
expect(result!.version, '1.1.0');
|
||
expect(result.versionCode, 2);
|
||
expect(result.forceUpdate, false);
|
||
});
|
||
|
||
test('should return null when no update available', () async {
|
||
// Arrange
|
||
when(() => mockDio.get(
|
||
any(),
|
||
queryParameters: any(named: 'queryParameters'),
|
||
)).thenAnswer((_) async => Response(
|
||
data: {
|
||
'code': 0,
|
||
'data': {'hasUpdate': false}
|
||
},
|
||
statusCode: 200,
|
||
requestOptions: RequestOptions(path: ''),
|
||
));
|
||
|
||
// Act
|
||
final result = await versionChecker.checkForUpdate();
|
||
|
||
// Assert
|
||
expect(result, isNull);
|
||
});
|
||
|
||
test('should return null on network error', () async {
|
||
// Arrange
|
||
when(() => mockDio.get(
|
||
any(),
|
||
queryParameters: any(named: 'queryParameters'),
|
||
)).thenThrow(DioException(
|
||
type: DioExceptionType.connectionError,
|
||
requestOptions: RequestOptions(path: ''),
|
||
));
|
||
|
||
// Act
|
||
final result = await versionChecker.checkForUpdate();
|
||
|
||
// Assert
|
||
expect(result, isNull);
|
||
});
|
||
});
|
||
}
|
||
```
|
||
|
||
### 4.3 TelemetryStorage 单元测试
|
||
|
||
```dart
|
||
// test/unit/core/telemetry/telemetry_storage_test.dart
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:rwa_android_app/core/telemetry/storage/telemetry_storage.dart';
|
||
import 'package:rwa_android_app/core/telemetry/models/telemetry_event.dart';
|
||
|
||
void main() {
|
||
late TelemetryStorage storage;
|
||
|
||
setUp(() {
|
||
storage = TelemetryStorage();
|
||
storage.clear(); // 清理之前的数据
|
||
});
|
||
|
||
group('TelemetryStorage', () {
|
||
test('should enqueue and dequeue events', () {
|
||
// Arrange
|
||
final event = TelemetryEvent(
|
||
eventId: 'test-1',
|
||
name: 'test_event',
|
||
type: EventType.userAction,
|
||
level: EventLevel.info,
|
||
installId: 'install-1',
|
||
deviceContextId: 'ctx-1',
|
||
timestamp: DateTime.now(),
|
||
);
|
||
|
||
// Act
|
||
storage.enqueueEvent(event);
|
||
final events = storage.dequeueEvents(10);
|
||
|
||
// Assert
|
||
expect(events.length, 1);
|
||
expect(events.first.eventId, 'test-1');
|
||
});
|
||
|
||
test('should respect max queue size', () {
|
||
// Arrange
|
||
storage.setMaxQueueSize(5);
|
||
|
||
// Act
|
||
for (int i = 0; i < 10; i++) {
|
||
storage.enqueueEvent(TelemetryEvent(
|
||
eventId: 'event-$i',
|
||
name: 'test_event',
|
||
type: EventType.userAction,
|
||
level: EventLevel.info,
|
||
installId: 'install-1',
|
||
deviceContextId: 'ctx-1',
|
||
timestamp: DateTime.now(),
|
||
));
|
||
}
|
||
|
||
// Assert
|
||
expect(storage.getQueueSize(), 5);
|
||
});
|
||
|
||
test('should dequeue in FIFO order', () {
|
||
// Arrange
|
||
for (int i = 0; i < 5; i++) {
|
||
storage.enqueueEvent(TelemetryEvent(
|
||
eventId: 'event-$i',
|
||
name: 'test_event',
|
||
type: EventType.userAction,
|
||
level: EventLevel.info,
|
||
installId: 'install-1',
|
||
deviceContextId: 'ctx-1',
|
||
timestamp: DateTime.now(),
|
||
));
|
||
}
|
||
|
||
// Act
|
||
final events = storage.dequeueEvents(3);
|
||
|
||
// Assert
|
||
expect(events[0].eventId, 'event-0');
|
||
expect(events[1].eventId, 'event-1');
|
||
expect(events[2].eventId, 'event-2');
|
||
});
|
||
});
|
||
}
|
||
```
|
||
|
||
### 4.4 SessionManager 单元测试
|
||
|
||
```dart
|
||
// test/unit/core/telemetry/session_manager_test.dart
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:rwa_android_app/core/telemetry/session/session_manager.dart';
|
||
|
||
void main() {
|
||
late SessionManager sessionManager;
|
||
|
||
setUp(() {
|
||
sessionManager = SessionManager(
|
||
sessionTimeout: const Duration(minutes: 30),
|
||
);
|
||
});
|
||
|
||
group('SessionManager', () {
|
||
test('should generate new session on start', () {
|
||
// Act
|
||
sessionManager.startSession();
|
||
|
||
// Assert
|
||
expect(sessionManager.currentSessionId, isNotNull);
|
||
expect(sessionManager.currentSessionId, isNotEmpty);
|
||
});
|
||
|
||
test('should keep same session on resume within timeout', () {
|
||
// Arrange
|
||
sessionManager.startSession();
|
||
final originalSessionId = sessionManager.currentSessionId;
|
||
sessionManager.pauseSession();
|
||
|
||
// Act
|
||
sessionManager.resumeSession();
|
||
|
||
// Assert
|
||
expect(sessionManager.currentSessionId, originalSessionId);
|
||
});
|
||
|
||
test('should generate new session on resume after timeout', () async {
|
||
// Arrange
|
||
sessionManager = SessionManager(
|
||
sessionTimeout: const Duration(milliseconds: 100),
|
||
);
|
||
sessionManager.startSession();
|
||
final originalSessionId = sessionManager.currentSessionId;
|
||
sessionManager.pauseSession();
|
||
|
||
// Wait for timeout
|
||
await Future.delayed(const Duration(milliseconds: 150));
|
||
|
||
// Act
|
||
sessionManager.resumeSession();
|
||
|
||
// Assert
|
||
expect(sessionManager.currentSessionId, isNot(originalSessionId));
|
||
});
|
||
|
||
test('should calculate session duration correctly', () async {
|
||
// Arrange
|
||
sessionManager.startSession();
|
||
|
||
// Wait some time
|
||
await Future.delayed(const Duration(seconds: 1));
|
||
|
||
// Act
|
||
final duration = sessionManager.getSessionDuration();
|
||
|
||
// Assert
|
||
expect(duration.inSeconds, greaterThanOrEqualTo(1));
|
||
});
|
||
});
|
||
}
|
||
```
|
||
|
||
### 4.5 运行测试
|
||
|
||
```bash
|
||
# 运行所有单元测试
|
||
flutter test
|
||
|
||
# 运行指定测试文件
|
||
flutter test test/unit/core/updater/version_checker_test.dart
|
||
|
||
# 运行测试并生成覆盖率报告
|
||
flutter test --coverage
|
||
|
||
# 查看覆盖率报告 (需要安装 lcov)
|
||
genhtml coverage/lcov.info -o coverage/html
|
||
open coverage/html/index.html
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Mock 服务器搭建
|
||
|
||
### 5.1 使用 JSON Server (快速搭建)
|
||
|
||
**安装**:
|
||
```bash
|
||
npm install -g json-server
|
||
```
|
||
|
||
**创建 db.json**:
|
||
```json
|
||
{
|
||
"app": {
|
||
"version": {
|
||
"check": {
|
||
"code": 0,
|
||
"data": {
|
||
"hasUpdate": true,
|
||
"version": "1.1.0",
|
||
"versionCode": 2,
|
||
"downloadUrl": "http://192.168.1.100:3000/releases/app.apk",
|
||
"fileSize": 52428800,
|
||
"fileSizeFriendly": "50.0 MB",
|
||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||
"forceUpdate": false,
|
||
"updateLog": "1. 新功能\\n2. Bug修复",
|
||
"releaseDate": "2024-01-15T10:00:00Z"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"analytics": {
|
||
"events": []
|
||
},
|
||
"presence": {
|
||
"heartbeat": { "code": 0, "message": "success" }
|
||
},
|
||
"telemetry": {
|
||
"config": {
|
||
"code": 0,
|
||
"data": {
|
||
"enabled": true,
|
||
"samplingRate": 1.0,
|
||
"enabledEventTypes": ["page_view", "user_action", "error", "crash", "session", "presence"],
|
||
"maxQueueSize": 1000,
|
||
"uploadBatchSize": 20,
|
||
"uploadIntervalSeconds": 30,
|
||
"heartbeatIntervalSeconds": 60,
|
||
"sessionTimeoutMinutes": 30
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**启动服务器**:
|
||
```bash
|
||
json-server --watch db.json --port 3000 --host 0.0.0.0
|
||
```
|
||
|
||
**修改应用配置**:
|
||
```dart
|
||
// lib/bootstrap.dart
|
||
const String _apiBaseUrl = 'http://192.168.1.100:3000'; // 你的电脑IP
|
||
```
|
||
|
||
### 5.2 使用 Python Flask (更灵活)
|
||
|
||
```python
|
||
# mock_server.py
|
||
|
||
from flask import Flask, request, jsonify
|
||
from datetime import datetime
|
||
import hashlib
|
||
|
||
app = Flask(__name__)
|
||
|
||
# 存储上报的事件
|
||
events_log = []
|
||
|
||
@app.route('/api/app/version/check', methods=['GET'])
|
||
def check_version():
|
||
current_version = request.args.get('currentVersion', '1.0.0')
|
||
current_code = int(request.args.get('currentVersionCode', '1'))
|
||
|
||
# 模拟新版本
|
||
if current_code < 2:
|
||
return jsonify({
|
||
'code': 0,
|
||
'data': {
|
||
'hasUpdate': True,
|
||
'version': '1.1.0',
|
||
'versionCode': 2,
|
||
'downloadUrl': 'http://192.168.1.100:5000/releases/app.apk',
|
||
'fileSize': 52428800,
|
||
'fileSizeFriendly': '50.0 MB',
|
||
'sha256': 'your-sha256-hash',
|
||
'forceUpdate': False,
|
||
'updateLog': '1. 新功能\n2. Bug修复',
|
||
'releaseDate': datetime.now().isoformat()
|
||
}
|
||
})
|
||
else:
|
||
return jsonify({
|
||
'code': 0,
|
||
'data': {'hasUpdate': False}
|
||
})
|
||
|
||
@app.route('/api/v1/analytics/events', methods=['POST'])
|
||
def receive_events():
|
||
data = request.json
|
||
events = data.get('events', [])
|
||
events_log.extend(events)
|
||
print(f"Received {len(events)} events. Total: {len(events_log)}")
|
||
for e in events:
|
||
print(f" - {e['name']} ({e['type']}) at {e['timestamp']}")
|
||
return jsonify({'code': 0, 'message': 'success'})
|
||
|
||
@app.route('/api/v1/presence/heartbeat', methods=['POST'])
|
||
def heartbeat():
|
||
data = request.json
|
||
print(f"Heartbeat from {data.get('installId')} at {data.get('timestamp')}")
|
||
return jsonify({'code': 0, 'message': 'success'})
|
||
|
||
@app.route('/api/telemetry/config', methods=['GET'])
|
||
def telemetry_config():
|
||
return jsonify({
|
||
'code': 0,
|
||
'data': {
|
||
'enabled': True,
|
||
'samplingRate': 1.0,
|
||
'enabledEventTypes': ['page_view', 'user_action', 'error', 'crash', 'session', 'presence'],
|
||
'maxQueueSize': 1000,
|
||
'uploadBatchSize': 20,
|
||
'uploadIntervalSeconds': 30,
|
||
'heartbeatIntervalSeconds': 60,
|
||
'sessionTimeoutMinutes': 30
|
||
}
|
||
})
|
||
|
||
@app.route('/releases/<filename>', methods=['GET'])
|
||
def download_apk(filename):
|
||
# 返回测试APK文件
|
||
return app.send_static_file(filename)
|
||
|
||
@app.route('/events', methods=['GET'])
|
||
def view_events():
|
||
"""调试用: 查看所有上报的事件"""
|
||
return jsonify({
|
||
'total': len(events_log),
|
||
'events': events_log[-100:] # 返回最近100条
|
||
})
|
||
|
||
if __name__ == '__main__':
|
||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||
```
|
||
|
||
**启动**:
|
||
```bash
|
||
pip install flask
|
||
python mock_server.py
|
||
```
|
||
|
||
### 5.3 网络调试工具
|
||
|
||
| 工具 | 用途 |
|
||
|-----|------|
|
||
| Charles | HTTPS 抓包、修改响应 |
|
||
| Fiddler | Windows 抓包工具 |
|
||
| mitmproxy | 命令行抓包工具 |
|
||
| Wireshark | 底层网络分析 |
|
||
|
||
**Charles 配置要点**:
|
||
1. 手机和电脑在同一局域网
|
||
2. 手机设置代理到电脑 IP:8888
|
||
3. 安装 Charles 根证书到手机
|
||
4. 使用 Map Remote 或 Rewrite 修改响应
|
||
|
||
---
|
||
|
||
## 6. 常见问题排查
|
||
|
||
### 6.1 APK 安装失败
|
||
|
||
**症状**: 下载成功但安装界面不弹出
|
||
|
||
**排查步骤**:
|
||
1. 检查 `REQUEST_INSTALL_PACKAGES` 权限
|
||
2. 检查 FileProvider 配置
|
||
3. 查看 Logcat 中的安装错误
|
||
|
||
```bash
|
||
adb logcat | grep -i "install"
|
||
```
|
||
|
||
### 6.2 心跳不发送
|
||
|
||
**症状**: 控制台没有心跳日志
|
||
|
||
**排查步骤**:
|
||
1. 确认遥测服务已初始化
|
||
2. 确认应用在前台
|
||
3. 检查远程配置是否禁用了 presence 类型
|
||
|
||
### 6.3 事件不上传
|
||
|
||
**症状**: 事件积累但不上传
|
||
|
||
**排查步骤**:
|
||
1. 检查网络连接
|
||
2. 检查队列大小是否达到阈值 (默认10)
|
||
3. 检查远程配置 `enabled` 是否为 true
|
||
4. 查看上传错误日志
|
||
|
||
### 6.4 设备信息采集失败
|
||
|
||
**症状**: DeviceContext 部分字段为空
|
||
|
||
**排查步骤**:
|
||
1. 确认使用真机测试(模拟器部分信息不可用)
|
||
2. 检查 device_info_plus 插件版本
|
||
3. 查看采集错误日志
|
||
|
||
---
|
||
|
||
## 附录: 测试数据示例
|
||
|
||
### 版本检测响应
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"hasUpdate": true,
|
||
"version": "1.1.0",
|
||
"versionCode": 2,
|
||
"downloadUrl": "https://cdn.rwadurian.com/releases/app-v1.1.0.apk",
|
||
"fileSize": 52428800,
|
||
"fileSizeFriendly": "50.0 MB",
|
||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||
"forceUpdate": false,
|
||
"updateLog": "1. 新增挖矿动画\n2. 优化性能\n3. 修复已知问题",
|
||
"releaseDate": "2024-01-15T10:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 遥测事件示例
|
||
|
||
```json
|
||
{
|
||
"events": [
|
||
{
|
||
"eventId": "550e8400-e29b-41d4-a716-446655440000",
|
||
"name": "session_start",
|
||
"type": "session",
|
||
"level": "info",
|
||
"installId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"deviceContextId": "ctx-xyz789",
|
||
"sessionId": "sess-abc123",
|
||
"userId": null,
|
||
"timestamp": "2024-01-15T10:00:00.000Z",
|
||
"properties": {
|
||
"trigger": "app_launch",
|
||
"is_first_launch": false
|
||
}
|
||
},
|
||
{
|
||
"eventId": "550e8400-e29b-41d4-a716-446655440001",
|
||
"name": "page_view",
|
||
"type": "page_view",
|
||
"level": "info",
|
||
"installId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"deviceContextId": "ctx-xyz789",
|
||
"sessionId": "sess-abc123",
|
||
"userId": null,
|
||
"timestamp": "2024-01-15T10:00:01.000Z",
|
||
"properties": {
|
||
"page": "/ranking",
|
||
"referrer": "/splash"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|