HarmonyOS趣味相机实战第22篇Hypium测试分层、CameraKit替身与真机回归矩阵摘要相机应用不能只靠“打开页面拍一张图”验证。权限拒绝、无前置镜头、PhotoAvailable 迟到、PixelMap 解码失败、Preferences 坏 JSON、页面退出中的倒计时、前后台重建等问题都具有时序性人工偶尔点一次很难稳定复现。本文基于D:/APP/1quweixiangji趣味相机工程盘点entry/src/test与entry/src/ohosTest的现状并建立四层测试金字塔纯函数与模型、服务适配器、ArkUI 工作流、CameraKit 真机回归。重点不是追求用例数量而是把不可控硬件边界包在小型适配器后让状态机、选型算法、迁移和资源所有权在本地快速测试。工程背景与测试文件文件当前内容结论entry/src/test/LocalUnit.test.ets水印快照创建与保存克隆有真实业务断言entry/src/test/List.test.ets注册 localUnitTest本地测试入口entry/src/ohosTest/ets/test/Ability.test.etsabc包含b模板断言尚未覆盖 Abilityentry/src/ohosTest/ets/test/List.test.ets注册 abilityTest真机测试入口PhotoAlbumService.etsPreferences、克隆与默认值适合本地单测CameraDeviceService.etsCameraManager 直接调用需要适配器替身CameraPreviewService.etsSession、回调、PixelMap适合集成和真机测试Index.ets页面工作流适合状态与组件测试环境与测试边界项目当前值说明测试框架ohos/hypium 1.0.25根 oh-package 配置Mock 依赖ohos/hamock 1.0.0可用于替身能力本地测试目录entry/src/test不依赖真机硬件设备测试目录entry/src/ohosTestAbility 与系统能力target SDK6.0.2(22)当前工程核心设备能力CAMERA需要真实权限和镜头一、先看现有有效测试项目已有it(photo snapshot keeps watermark metadata,0,(){constphotoPhotoAlbumService.createPhoto(1,[],无滤镜,无相框,标准模式,real,测试拍照,0,标准,0,12MP (4:3),{enabled:true,template:work,title:工作,locationText:杭州,note:今日拍摄,timeText:06-30 19:49});expect(photo.title).assertEqual(水印照片 1);expect(photo.layerCount).assertEqual(0);expect(photo.watermark?.title).assertEqual(工作);expect(photo.watermark?.locationText).assertEqual(杭州);});以及保存克隆constsavedPhotoAlbumService.savePhoto(photo);expect(saved.watermark?.template).assertEqual(check);expect(saved.watermark?.note).assertEqual(记录一下);expect(saved.status).assertEqual(saved);这两条用例测试了业务而不是框架本身应保留并扩展。二、删除模板断言换成真实Ability验收当前 ohosTestit(assertContain,0,(){letaabc;letbb;expect(a).assertContain(b);expect(a).assertEqual(a);});这只能证明 Hypium 能运行不能证明应用可用。冒烟用例应至少覆盖EntryAbility能启动 - WindowStage加载pages/Index - 首页关键元素存在 - 不授权相机时应用不崩溃 - 相册和文档空态可显示模板用例在框架接通后应移除避免测试数量看起来增加但没有风险覆盖。三、测试金字塔按硬件依赖分层层级比例建议运行位置例子纯函数/模型60%本地Profile评分、坐标映射、迁移服务适配器20%本地权限、设备、Preferences替身ArkUI工作流10%模拟或设备弹层、Tab、状态闭环CameraKit真机10%真机预览、拍照、回调、释放硬件用例最慢也最不稳定因此只保留系统契约和关键路径组合爆炸放到纯函数层覆盖。四、先抽出纯函数以下逻辑不需要相机Profile 宽高比和像素评分。camera.Rect归一化。旋转、镜像、Cover 坐标映射。前后镜头可用性决策。水印摘要与默认值。Preferences JSON 迁移。装饰套装引用校验。人脸与骨骼目标匹配评分。拍照状态提示文案。把它们放到独立模块后本地测试可以在毫秒级完成。五、CameraDeviceService需要依赖反转当前直接调用constmanager:camera.CameraManagercamera.getCameraManager(context);constsupportedDevices:camera.CameraDevice[]manager.getSupportedCameras();可定义最小端口interfaceCameraDevicePort{totalCount():number;hasPosition(position:CameraPositionKey):boolean;}classCameraKitDeviceAdapterimplementsCameraDevicePort{constructor(privatemanager:camera.CameraManager){}totalCount():number{returnthis.manager.getSupportedCameras().length;}hasPosition(position:CameraPositionKey):boolean{// 调用getCameraDevice并转换异常returntrue;}}业务函数只依赖端口functiondiscoverWithPort(port:CameraDevicePort,preferred:CameraPositionKey):CameraDeviceState{consthasFrontport.hasPosition(front);consthasBackport.hasPosition(back);returnbuildDeviceState(port.totalCount(),hasFront,hasBack,preferred);}六、设备发现决策表frontbackpreferredactiveavailablefalsefalsebackback或占位falsetruefalsebackfronttruefalsetruefrontbacktruetruetruefrontfronttruetruetruebackbacktrueHypium 用例it(falls back to front when back is absent,0,(){conststatebuildDeviceState(1,true,false,back);expect(state.available).assertTrue();expect(state.activePosition).assertEqual(front);});nextPosition()也覆盖单镜头不切换和双镜头往返。七、权限服务返回业务结果而不是抛异常权限测试至少有已授权 用户首次同意 用户拒绝 系统API抛BusinessError Context不可用适配器interfaceCameraPermissionPort{check():Promiseboolean;request():Promiseboolean;}classFakePermissionPortimplementsCameraPermissionPort{constructor(privatechecked:boolean,privaterequested:boolean,privateshouldThrow:booleanfalse){}asynccheck():Promiseboolean{if(this.shouldThrow){thrownewError(check failed);}returnthis.checked;}asyncrequest():Promiseboolean{returnthis.requested;}}业务层断言状态和用户文案不需要真的弹系统授权框。八、Preferences测试使用内存仓库定义interfaceKeyValueStore{get(key:string,fallback:string):string;put(key:string,value:string):Promisevoid;flush():Promisevoid;}classMemoryStoreimplementsKeyValueStore{values:Mapstring,stringnewMap();flushCount:number0;get(key:string,fallback:string):string{returnthis.values.get(key)??fallback;}asyncput(key:string,value:string):Promisevoid{this.values.set(key,value);}asyncflush():Promisevoid{this.flushCount1;}}覆盖空存储返回空数组。坏 JSON 回退且不崩溃。保存后最新照片位于首位。相册最多 60 条。文档最多 80 条。delete 后调用 flush。clone 返回值修改不会污染缓存。九、深拷贝用例防止状态串改it(list result cannot mutate cache watermark,0,async(){constfirstawaitservice.listPhotos();if(first[0].watermark){first[0].watermark.locationTextchanged;}constsecondawaitservice.listPhotos();expect(second[0].watermark?.locationText).assertEqual(original);});数组复制不等于嵌套对象复制。水印快照必须单独 clone。十、Profile评分测试不依赖真实设备输入候选尺寸断言it(profile choice is independent from source order,0,(){constleftchoosePreviewProfile([p720,p1080,p480]);constrightchoosePreviewProfile([p480,p720,p1080]);expect(left.size.width).assertEqual(right.size.width);expect(left.size.height).assertEqual(right.size.height);});此外覆盖预算、比例、空数组和全部超预算降级。十一、坐标映射使用表驱动interfaceMappingCase{name:string;rotation:camera.ImageRotation;mirror:boolean;inputX:number;inputY:number;expectedX:number;expectedY:number;}为四个角点生成 0/90/180/270 和前后镜头组合。表驱动比复制几十个it()更容易审查。十二、CameraPreviewService拆成会话端口最难测试的是直接持有 CameraKit 类。可定义interfaceCameraSessionPort{open():Promisevoid;configure():Promisevoid;start():Promisevoid;capture():Promisevoid;stop():Promisevoid;release():Promisevoid;}Fake 记录调用顺序classFakeSessionPortimplementsCameraSessionPort{calls:string[][];failAt:string;asyncinvoke(name:string):Promisevoid{this.calls.push(name);if(this.failAtname){thrownewError(${name}failed);}}asyncopen(){awaitthis.invoke(open);}asyncconfigure(){awaitthis.invoke(configure);}asyncstart(){awaitthis.invoke(start);}asynccapture(){awaitthis.invoke(capture);}asyncstop(){awaitthis.invoke(stop);}asyncrelease(){awaitthis.invoke(release);}}断言失败点后的清理顺序而不是模拟整个 CameraManager。十三、会话故障注入矩阵故障点预期input.open返回 error清理已创建输入createPreviewOutput释放 inputcommitConfig释放 session、outputs、inputsession.start完整停止与释放metadata.start基础预览继续识别降级capture预览保持 runningphotoAvailable error本次拍照失败不重建预览PixelMap decode释放 Photo 和 ImageSource十四、异步时间使用可控时钟倒计时和识别采样若依赖真实setTimeout测试慢且不稳定。抽象interfaceClock{now():number;delay(ms:number):Promisevoid;}classFakeClockimplementsClock{current:number0;now():number{returnthis.current;}asyncdelay(ms:number):Promisevoid{this.currentms;}}可立即测试3/5/10 秒倒计时序列。页面退出后 generation 失效。2200ms 识别结果过期。拍照回调超时。十五、PixelMap用Lease替身验证释放次数classFakeLease{releaseCount:number0;asyncrelease():Promisevoid{this.releaseCount1;}}断言解码失败释放一次。成功转移时服务不释放。弹层关闭释放一次。关闭后页面退出不会释放第二次。迟到任务无人接收时释放。十六、ArkUI工作流用状态模型驱动将页面动作从 Builder 中抽出typeCaptureUiStatusidle|countdown|capturing|preview|saving|error;interfaceCaptureUiState{status:CaptureUiStatus;countdown:number;message:string;hasPixelMap:boolean;}测试事件点击拍照 - countdown/capturing 回调成功 - preview 点击重拍 - idle并释放 点击保存 - saving - idle 保存失败 - preview或error可重试 页面退出 - idle且资源清空UI 只渲染状态核心转换可本地测试。十七、ohosTest只验证真实系统契约真机用例建议EntryAbility 冷启动成功。CAMERA 权限拒绝后页面可继续操作相册。授权后 Preview 进入 running。PhotoOutput 回调在超时内到达。前置拍照 mirror 方向正确。前后台切换后相机可恢复。连续切镜头后无重复回调。停止后相机资源可被其他应用使用。不要在真机上重复纯函数的几十种组合。十八、测试数据不能包含真实隐私固定水印夹具可以使用constwatermark:WatermarkSnapshot{enabled:true,template:work,title:测试水印,locationText:测试地点,note:自动化测试,timeText:01-01 08:00};不要把真实照片、真实地点、用户相册 URI 或浏览器登录信息提交到测试仓库。十九、测试命名表达业务结果推荐falls back to back camera when front is absent keeps latest 60 photos after persistence releases pixel map when preview is closed ignores stale capture result after page exit does not fail preview when face metadata is unsupported避免test1 assertContain cameraTest works失败报告应让开发者不打开测试代码也能理解风险。二十、持续回归门槛提交前ArkTS静态检查 - src/test本地单元测试 - Debug构建候选发布包Release构建 - ohosTest核心真机用例 - 前后镜头兼容矩阵 - 连续拍照和前后台压力测试 - 权限与隐私检查测试失败不能只截图记录应保存非隐私日志、设备能力摘要、用例步骤和版本号。二十一、常见问题排查现象原因改进测试全绿但相机黑屏只有模板断言增加真机Session契约用例本地测试无法导入CameraKit业务直接依赖系统类引入最小端口和适配器倒计时用例需要等10秒使用真实Timer注入FakeClock测试偶发失败依赖固定sleep等待状态或控制Promise坏JSON覆盖不到Preferences不可替换注入MemoryStore连续用例相互污染静态缓存未重置提供测试生命周期重建资源泄漏无法断言直接使用PixelMap引入Lease和释放计数真机用例数量爆炸所有组合都上设备组合放纯函数层二十二、上线前验收清单删除无业务价值的 Hypium 模板断言。保留并扩展水印快照真实测试。Profile、坐标、迁移和状态机抽为纯函数。CameraManager 和权限 API 通过最小端口隔离。Preferences 测试使用内存仓库。深拷贝测试覆盖嵌套 WatermarkSnapshot。会话启动每个失败点都有清理断言。Timer 和时间戳通过可控 Clock 测试。PixelMap 所有权有释放次数测试。ArkUI 工作流使用明确状态模型。ohosTest 聚焦真实系统和硬件契约。真机覆盖权限拒绝、切镜头、拍照和前后台。测试夹具不包含真实照片和地点。发布构建前执行固定回归门槛。总结HarmonyOS 相机测试的关键不是把 CameraKit 全部 Mock 掉而是划清可控和不可控边界。Profile 评分、坐标映射、数据迁移、状态机和资源所有权都能在本地稳定测试真实权限、Session、PhotoAvailable 和设备差异则由少量高价值 ohosTest 验证。当前工程已经有两条有效水印快照用例下一步应删除模板断言并引入端口、FakeClock、MemoryStore 和 PixelMap Lease。测试分层后故障注入不再依赖手工碰运气真机回归也能聚焦真正需要硬件证明的契约。