24 KiB
24 KiB
测试指南
本文档提供 APK 升级模块、遥测模块的手动测试和自动化测试方法。
目录
1. 测试环境准备
1.1 开发环境
# 确保 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
# 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)
测试步骤:
- 启动应用,进入主页面(龙虎榜)
- 等待 3 秒后观察是否弹出更新对话框
预期结果:
- 弹出更新对话框
- 显示新版本号 "1.1.0"
- 显示文件大小
- 显示更新日志(如有)
验证日志:
flutter: 📊 Checking for update...
flutter: 📊 New version available: 1.1.0
测试用例 2: 版本检测 - 已是最新版本
前置条件:
- 当前安装版本与服务器最新版本一致
测试步骤:
- 启动应用,进入主页面
- 等待 3 秒
预期结果:
- 不弹出更新对话框
验证日志:
flutter: Already latest version
测试用例 3: 强制更新
前置条件:
- 服务器配置
forceUpdate: true
测试步骤:
- 启动应用,触发版本检测
- 尝试点击对话框外部关闭
- 尝试按返回键关闭
预期结果:
- 对话框标题显示"发现重要更新"
- 无法通过点击外部或返回键关闭对话框
- 没有"稍后"按钮
测试用例 4: APK 下载与安装
前置条件:
- 服务器已配置有效的 APK 下载链接
- APK 已正确签名
测试步骤:
- 点击"立即更新"按钮
- 观察下载进度
- 下载完成后观察安装流程
预期结果:
- 显示下载进度对话框
- 进度百分比实时更新
- 下载完成后自动弹出系统安装界面
验证日志:
flutter: 📥 Downloading APK from: https://...
flutter: 📥 Download progress: 50%
flutter: ✅ APK downloaded successfully
flutter: 🔐 SHA-256 verification passed
flutter: 📲 Installing APK...
测试用例 5: 下载取消
测试步骤:
- 开始下载更新
- 在下载过程中点击"取消"按钮
预期结果:
- 下载立即停止
- 显示"下载已取消"
- 临时文件被清理
测试用例 6: 下载失败重试
前置条件:
- 模拟网络不稳定或服务器错误
测试步骤:
- 开始下载更新
- 断开网络或服务器返回错误
- 观察错误提示
- 点击"重试"按钮
预期结果:
- 显示"下载失败,请稍后重试"
- 出现"重试"按钮
- 点击重试后重新开始下载
测试用例 7: SHA-256 校验失败
前置条件:
- 服务器返回的 SHA-256 与实际文件不匹配
测试步骤:
- 下载 APK
- 等待校验完成
预期结果:
- 显示下载失败
- 日志显示校验失败信息
验证日志:
flutter: ❌ SHA-256 verification failed
flutter: Expected: abc123...
flutter: Actual: def456...
测试用例 8: 应用市场来源检测
前置条件:
- 从 Google Play 安装的应用
测试步骤:
- 启动应用
- 触发版本检测
预期结果:
- 显示"检测到您的应用来自应用市场"提示
- 按钮变为"前往应用市场"
2.2 调试技巧
强制触发版本检测
在代码中临时修改版本号进行测试:
// 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 抓包
- 配置手机代理到电脑
- 监控
/api/app/version/check请求 - 可修改响应数据测试不同场景
3. 遥测模块测试
3.1 手动测试清单
测试用例 1: 设备信息采集
测试步骤:
- 首次启动应用
- 等待 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: 会话开始事件
测试步骤:
- 完全关闭应用
- 重新启动应用
预期结果:
- 生成新的 sessionId
- 触发
session_start事件
验证日志:
flutter: 📊 New session started: session-abc123
flutter: 📊 Event tracked: session_start
测试用例 3: 会话结束事件
测试步骤:
- 应用在前台运行
- 按 Home 键将应用切到后台
预期结果:
- 触发
session_end事件 - 事件包含会话时长
验证日志:
flutter: 📊 Session paused, duration: 120 seconds
flutter: 📊 Event tracked: session_end
测试用例 4: 会话恢复 (30分钟内)
测试步骤:
- 将应用切到后台
- 5 分钟内切回前台
预期结果:
- 恢复原有 session
- 触发
session_resume事件
验证日志:
flutter: 📊 Session resumed: session-abc123
flutter: 📊 Event tracked: session_resume
测试用例 5: 新会话 (超过30分钟)
测试步骤:
- 将应用切到后台
- 等待超过 30 分钟(或修改超时配置为较短时间测试)
- 切回前台
预期结果:
- 生成新的 sessionId
- 触发
session_start事件(非 resume)
测试用例 6: 心跳发送
测试步骤:
- 保持应用在前台
- 观察 60 秒内的日志
预期结果:
- 每 60 秒发送一次心跳
验证日志:
flutter: 💓 Heartbeat sent
flutter: 💓 Heartbeat sent
...
测试用例 7: 心跳暂停 (后台)
测试步骤:
- 应用在前台,心跳正常发送
- 将应用切到后台
- 等待 2 分钟
- 切回前台
预期结果:
- 后台期间不发送心跳
- 切回前台后立即发送心跳并恢复定时器
验证日志:
flutter: 💓 Heartbeat paused (app in background)
... (后台期间无心跳日志)
flutter: 💓 Heartbeat resumed (app in foreground)
flutter: 💓 Heartbeat sent
测试用例 8: 事件批量上传
测试步骤:
- 在应用中执行多个操作(页面切换、按钮点击等)
- 等待 30 秒或事件队列达到 10 条
预期结果:
- 触发批量上传
- 上传成功后清理本地队列
验证日志:
flutter: 📊 Queue size: 10, triggering upload
flutter: 📊 Uploading 10 events...
flutter: ✅ Uploaded 10 telemetry events
测试用例 9: 上传失败重试
前置条件:
- 模拟网络断开或服务器错误
测试步骤:
- 积累一些事件
- 断开网络
- 等待上传触发
预期结果:
- 上传失败但事件保留在本地队列
- 网络恢复后下次触发时重新上传
验证日志:
flutter: ❌ Upload error (DioException): Connection refused
flutter: 📊 Events kept in queue for retry
测试用例 10: 远程配置同步
测试步骤:
- 启动应用
- 修改服务器配置(如
samplingRate: 0.5) - 等待配置同步周期(默认 1 小时,测试时可改短)
预期结果:
- 获取并应用新配置
验证日志:
flutter: 📊 Fetching telemetry config...
flutter: 📊 Config updated: samplingRate=0.5
测试用例 11: 采样率过滤
前置条件:
- 配置
samplingRate: 0.0
测试步骤:
- 应用新配置
- 尝试触发事件
预期结果:
- 所有事件被过滤,不进入队列
验证日志:
flutter: 📊 Event filtered by sampling rate
测试用例 12: 事件类型过滤
前置条件:
- 配置
enabledEventTypes: ['error', 'crash']
测试步骤:
- 触发
page_view事件 - 触发
error事件
预期结果:
page_view被过滤error被记录
3.2 调试技巧
查看本地事件队列
// 临时添加调试代码
final storage = TelemetryStorage();
final events = storage.peekEvents(100);
for (var e in events) {
debugPrint('Event: ${e.name} - ${e.type} - ${e.timestamp}');
}
强制触发上传
// 在需要的地方调用
await TelemetryService().forceUpload();
缩短配置同步周期
// 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 单元测试
// 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 单元测试
// 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 单元测试
// 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 运行测试
# 运行所有单元测试
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 (快速搭建)
安装:
npm install -g json-server
创建 db.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
}
}
}
}
启动服务器:
json-server --watch db.json --port 3000 --host 0.0.0.0
修改应用配置:
// lib/bootstrap.dart
const String _apiBaseUrl = 'http://192.168.1.100:3000'; // 你的电脑IP
5.2 使用 Python Flask (更灵活)
# 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)
启动:
pip install flask
python mock_server.py
5.3 网络调试工具
| 工具 | 用途 |
|---|---|
| Charles | HTTPS 抓包、修改响应 |
| Fiddler | Windows 抓包工具 |
| mitmproxy | 命令行抓包工具 |
| Wireshark | 底层网络分析 |
Charles 配置要点:
- 手机和电脑在同一局域网
- 手机设置代理到电脑 IP:8888
- 安装 Charles 根证书到手机
- 使用 Map Remote 或 Rewrite 修改响应
6. 常见问题排查
6.1 APK 安装失败
症状: 下载成功但安装界面不弹出
排查步骤:
- 检查
REQUEST_INSTALL_PACKAGES权限 - 检查 FileProvider 配置
- 查看 Logcat 中的安装错误
adb logcat | grep -i "install"
6.2 心跳不发送
症状: 控制台没有心跳日志
排查步骤:
- 确认遥测服务已初始化
- 确认应用在前台
- 检查远程配置是否禁用了 presence 类型
6.3 事件不上传
症状: 事件积累但不上传
排查步骤:
- 检查网络连接
- 检查队列大小是否达到阈值 (默认10)
- 检查远程配置
enabled是否为 true - 查看上传错误日志
6.4 设备信息采集失败
症状: DeviceContext 部分字段为空
排查步骤:
- 确认使用真机测试(模拟器部分信息不可用)
- 检查 device_info_plus 插件版本
- 查看采集错误日志
附录: 测试数据示例
版本检测响应
{
"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"
}
}
遥测事件示例
{
"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"
}
}
]
}