# 测试指南 > 本文档提供 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 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/', 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" } } ] } ```