rwadurian/frontend/mobile-app/docs/testing_guide.md

24 KiB
Raw Blame History

测试指南

本文档提供 APK 升级模块、遥测模块的手动测试和自动化测试方法。


目录

  1. 测试环境准备
  2. APK 升级模块测试
  3. 遥测模块测试
  4. 自动化单元测试
  5. Mock 服务器搭建

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)

测试步骤:

  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 调试技巧

强制触发版本检测

在代码中临时修改版本号进行测试:

// 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 调试技巧

查看本地事件队列

// 临时添加调试代码
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 配置要点:

  1. 手机和电脑在同一局域网
  2. 手机设置代理到电脑 IP:8888
  3. 安装 Charles 根证书到手机
  4. 使用 Map Remote 或 Rewrite 修改响应

6. 常见问题排查

6.1 APK 安装失败

症状: 下载成功但安装界面不弹出

排查步骤:

  1. 检查 REQUEST_INSTALL_PACKAGES 权限
  2. 检查 FileProvider 配置
  3. 查看 Logcat 中的安装错误
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. 查看采集错误日志

附录: 测试数据示例

版本检测响应

{
  "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"
      }
    }
  ]
}