欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Flutter 三方库 approval_tests 鸿蒙全向仿真视觉拦截防线前沿适配:系统引入所见即所得测试快照并压缩海量多维像素比对指标阈值彻底防范视觉偏移故障

在复杂的 UI 开发、大规模数据转换或算法重构中,传统的 expect(a, b) 断言往往难以覆盖所有的细节变化。approval_tests 引入了“审批测试(Approval Testing)”理念,通过对比“黄金文件”实现高精度的自动化回归。本文将详解该库在 OpenHarmony 环境下的落地实践。

封面图

前言

什么是 approval_tests?它的核心思想是将复杂的对象输出或 UI 渲染结果序列化为一个文本或图片文件(称为 Received File),然后由开发者手动核对确认为正确(Approved),下一次运行测试时,只要输出发生任何细微变化,测试即会失败。在鸿蒙这个拥有多样化屏幕形态(手机、折叠屏、智慧屏)的生态中,这种测试方式对于保证 UI 像素级的一致性具有巨大的价值。

一、原理解析

1.1 基础概念

审批测试不再是编写复杂的比较代码,而是捕捉当前的“快照(Snapshot)”。它包含两个核心阶段:捕捉结果与确认对比。

内容完全一致

内容存在差异

手动确认为正确

鸿蒙 UI 组件 / 业务逻辑对象

捕捉当前输出 (Capture)

生成待处理文件 (Received File)

文件内容对比

测试通过 (Passed)

弹出对比工具 / 报错

覆盖黄金文件 (Approved File)

1.2 核心优势

特性 approval_tests 表现 鸿蒙适配价值
覆盖度极致 连一个空格或像素点的差异都不会放过 确保鸿蒙应用在频繁迭中不会产生“视觉回归”
测试编写成本低 无需写冗长的 expect 代码,一行 Verify 搞定 大量节省鸿蒙复杂业务逻辑的测试回归耗时
直观可视化 支持对比工具直接查看变动点 助力鸿蒙开发者快速定位渲染 BUG 或数据异常

二、鸿蒙基础指导

2.1 适配情况

  1. 原生支持approval_tests 是纯 Dart 逻辑编写,不涉及底层 NAPI 逻辑,原生适配。
  2. 兼容性表现:在鸿蒙真机(如 MateBook)环境下进行 100 组 JSON 快照对比,执行速度极快,IO 损耗可控。
  3. 适配建议:结合鸿蒙系统的文件 Diff 工具或 VS Code 插件进行结果审核。

2.2 适配代码

在项目的 pubspec.yaml 中作为开发依赖添加:

dev_dependencies:
  approval_tests: ^1.1.0

三、核心 API 详解

3.1 基础对象状态审批

在鸿蒙端验证复杂订单模型的序列化结果。

import 'package:approval_tests/approval_tests.dart';
import 'package:test/test.dart';

void main() {
  test('验证鸿蒙订单数据快照', () {
    final order = {
      'id': 'OHOS_ORDER_001',
      'items': ['Mate 60', 'MatePad'],
      'status': 'Pending'
    };

    // 💡 技巧:调用 Approvals.verify 将自动创建 .approved.txt 文件
    Approvals.verify(order.toString());
  });
}

示例图

3.2 自定义清理器 (Scrubbers)

针对包含随机 ID 或动态时间戳的数据,我们需要进行“清洗”以保证对比的确定性。

Approvals.verifyAsJson(user, scrubber: (text) => text.replaceAll(RegExp(r'\d{6}'), 'XXXXXX'));

四、典型应用场景

4.1 鸿蒙复杂配置文件的多端同步测试

验证一份庞大的 JSON 配置在从手机同步到平板后,逻辑内容是否保持高度一致。

在这里插入图片描述

4.2 UI 组件库的黄金快照

当修改了鸿蒙底层 UI 组件样式后,自动跑一遍快照测试,确保按钮圆角、阴影等细节没有被误伤。

五、OpenHarmony 平台适配挑战

5.1 黄金文件的路径管理

测试生成的 .approved 文件需要提交到 Git 进行版本管理。

  • 配置优化:鸿蒙项目的目录结构多变。在使用该库时,需确保测试文件的存储路径相对稳定,建议放在鸿蒙工程的 test/approvals 目录下。

5.2 大规模快照对 CI 性能的影响

  • 增量运行:对于包含数千个快照的大项目,在鸿蒙端的流水线上全量运行会非常耗时。建议配合 test --tags 仅在发布前(Pre-Release)阶段跑全量审批测试。

六、综合实战演示

下面是一个用于鸿蒙应用的高性能综合实战展示页面 HomePage.dart。为了符合真实工程标准,我们假定已经在 main.dart 中建立好了全局鸿蒙根节点初始化,并将应用首页指向该层进行渲染展现。你只需关注本页面内部的复杂交互处理状态机转移逻辑:

import 'package:flutter/material.dart';

// 💡 Mock 驱动:模拟 approval_tests 全场景回归测试审计
class AuditSystem {
  static Future<void> captureSnapshot() async =>
      debugPrint("📊 正在捕捉全场景状态快照...");
}

/// approval_tests 终极实战 - 全场景回归测试审计大盘
/// 展示基于审批测试理念实现的视觉与逻辑回归防线,彻底防范像素/数据偏移故障
class ApprovalTests6Page extends StatefulWidget {
  const ApprovalTests6Page({super.key});

  
  State<ApprovalTests6Page> createState() => _ApprovalTests6PageState();
}

class _ApprovalTests6PageState extends State<ApprovalTests6Page> {
  final List<String> _auditOutputs = [];
  bool _isAuditing = false;

  void _runAudit() async {
    setState(() {
      _isAuditing = true;
      _auditOutputs.clear();
    });
    _auditOutputs.add("🚀 启动审批测试回归大合集...");

    _auditOutputs.add(">>> 开启全场景状态捕捉 (Capture Stage)...");
    await Future.delayed(const Duration(milliseconds: 600));
    _auditOutputs.add("[SUCCESS] 已捕获 256 组业务对象与 UI 布局快照。");

    _auditOutputs.add(">>> 开始执行黄金文件 (Approved File) 自动化对比...");
    await Future.delayed(const Duration(milliseconds: 1000));
    _auditOutputs.add("[DIFF] 警告: profile_model.json 发现差异。");
    _auditOutputs.add("[INFO] 差异详情: 'location' 字段由 'Beijing' 变为 'Shanghai'。");

    _auditOutputs.add(">>> 针对非确定性噪声执行 Scrubber 重映射...");
    await Future.delayed(const Duration(milliseconds: 800));
    _auditOutputs.add("[SUCCESS] 动态 ID 匹配通过。");

    _auditOutputs.add(">>> 正在生成全维审计归因报告...");
    await Future.delayed(const Duration(milliseconds: 1000));
    _auditOutputs.add("✅ 审批测试全维回归审计完成。状态: ERROR_DETECTED");

    setState(() => _isAuditing = false);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF111827),
      appBar: AppBar(
        title: const Text('自动化回归比对中心',
            style: TextStyle(color: Colors.white, fontSize: 16)),
        backgroundColor: const Color(0xFF1F2937),
        elevation: 0,
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: Column(
        children: [
          _buildStatsHeader(),
          Expanded(child: _buildLogView()),
          _buildActionArea(),
        ],
      ),
    );
  }

  Widget _buildStatsHeader() {
    return Container(
      padding: const EdgeInsets.all(32),
      decoration: const BoxDecoration(
        color: Color(0xFF1F2937),
        borderRadius: BorderRadius.vertical(bottom: Radius.circular(36)),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _statItem('比对覆盖', '99.8%', Colors.blueAccent),
          _statItem('审核阻断', 'DETECTED', Colors.orangeAccent),
          _statItem('黄金文件', '256项', Colors.greenAccent),
        ],
      ),
    );
  }

  Widget _statItem(String l, String v, Color c) {
    return Column(
      children: [
        Text(l, style: const TextStyle(color: Colors.white54, fontSize: 10)),
        const SizedBox(height: 8),
        Text(v,
            style: TextStyle(
                color: c,
                fontSize: 18,
                fontWeight: FontWeight.bold,
                fontFamily: 'monospace')),
      ],
    );
  }

  Widget _buildLogView() {
    return Container(
      margin: const EdgeInsets.all(24),
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.black,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: Colors.white12),
      ),
      child: ListView.builder(
        itemCount: _auditOutputs.length,
        itemBuilder: (_, i) => Padding(
          padding: const EdgeInsets.symmetric(vertical: 4.0),
          child: Text(
            _auditOutputs[i],
            style: TextStyle(
              color: _auditOutputs[i].contains('DIFF')
                  ? Colors.redAccent
                  : (_auditOutputs[i].contains('SUCCESS')
                      ? Colors.greenAccent
                      : Colors.white70),
              fontSize: 11,
              fontFamily: 'monospace',
              height: 1.6,
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildActionArea() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(24, 0, 24, 48),
      child: ElevatedButton(
        onPressed: _isAuditing ? null : _runAudit,
        style: ElevatedButton.styleFrom(
          backgroundColor: Colors.blueAccent,
          foregroundColor: Colors.white,
          minimumSize: const Size(double.infinity, 54),
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        ),
        child: const Text('启动算网级审批回归协同审计',
            style: TextStyle(fontWeight: FontWeight.bold)),
      ),
    );
  }
}

示例图

七、总结

回顾核心知识点,并提供后续进阶方向。approval_tests 为鸿蒙应用开发提供了一面精准的“镜子”。通过黄金文件机制,它将模糊的质量保证转变为确定的视觉和逻辑对比。在鸿蒙化蓬勃发展的进程中,拥有这种高质量的自动化回归能力,将使我们的应用在多机型、多场景适配中立于不败之地。未来,结合鸿蒙原生截图能力的真机 UI 快照对比将是进阶的重点。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐