资讯动态

VisionCamera 真机 Harness 测试指南:为命令式 API 编写端到端回归测试

发布时间:2026/9/16 0:03:55 来源:尧图企业网站定制
VisionCamera 真机 Harness 测试指南为命令式 API 编写端到端回归测试【免费下载链接】react-native-vision-camera A powerful, high-performance React Native Camera library.项目地址: https://gitcode.com/GitHub_Trending/re/react-native-vision-camera导读本文介绍 react-native-vision-camera 仓库中apps/simple-camera/__tests__/目录下的 Harness 真机测试套件它通过 react-native-harness 将 Jest 兼容的测试运行器嵌入示例应用在真实手机本地 adb 设备或 AWS Device Farm 设备上对VisionCamera的命令式 API 做端到端回归验证。读完本文你将掌握这套测试的组织方式、九条编写规范、本机与 CI 的运行方法以及如何用最小失败测试 PR的流程上报相机相关 bug。为什么需要一套跑在真机上的测试相机库的 API 与真实硬件强耦合分辨率、帧格式、闪光灯、HDR、防抖等能力因设备而异模拟器和类型系统都无法验证capturePhoto拍出的照片是否真的包含像素数据。apps/simple-camera/__tests__/README.md明确了这套测试存在的两个目标按优先级排列公共 API 回归在 CI 上自动失败该库在真实硬件上支持的每一项VisionCamera特性都对应一个测试。如果一次重构破坏了capturePhoto下一次 PR 的 CI 运行就会变红。Bug 报告变成可执行代码任何人发现 bug 时预期不是新建一个独立复现仓库而是打开一个 PR在本目录下添加一个最小的失败测试复现问题。维护者在同一分支上修复 bug 直到 CI 变绿测试随修复一起合并。这样同一个 bug 永远不会再次静默回归。如果你要上报 bug打开一个 PR按下文规则在本目录下添加尽可能小的it(...)块然后在 issue 中引用该 PR——PR 上的 CI 运行本身就是复现。无需创建单独仓库。测试布局一个文件对应一个 API 领域测试按领域拆分每个文件测试命令式VisionCameraAPI 的一个切片文件命名遵循__tests__/**/*.harness.{ts,tsx}模式文件覆盖内容visioncamera.devices.harness.tsVisionCamera.createDeviceFactory、设备枚举、每设备能力、getCameraForId、addOnCameraDevicesChangedListener、getSupportedExtensions、userPreferredCameravisioncamera.session.harness.tscreateCameraSession、configure、start、stop、addOnStartedListener/addOnStoppedListener/addOnErrorListener、中断监听器、运行中重配置、多摄像头visioncamera.photo.harness.tscreatePhotoOutput、capturePhoto/capturePhotoToFile、容器格式JPEG、HEIC、DNG、闪光灯 / 镜像 / 质量 / 分辨率选项、拍摄生命周期回调、预览图visioncamera.video.harness.tscreateVideoOutput、Recorder生命周期、音频、maxDuration/maxFileSize自动停止、暂停 / 恢复 / 取消、持久化 Recorder、更高分辨率编码visioncamera.frame.harness.tscreateFrameOutput、通过react-native-vision-camera-worklets安装 worklet、YUV / RGB / 原生像素格式、scheduleOnRN、createSynchronizable、setOnFrameDroppedCallback、enablePreviewSizedOutputBuffersvisioncamera.multi-output.harness.ts组合 photo、video、frame 输出的多输出会话、替换某个输出而其他输出保持挂接、跨会话重启的持久录制visioncamera.constraints.harness.tsVisionCamera.resolveConstraintsonSessionConfigSelected、FPS / HDR / 防抖 / binned / pixelFormat / resolutionBias 约束visioncamera.controller.harness.tsCameraController——变焦、手电筒、曝光补偿、对焦测光、低光增强、主体区域监听器visioncamera.hooks.harness.tsxuseCameraDevice(...)对位置和物理设备过滤变化的 React 钩子响应性以及useCamera(...).onUIRotationChangedvisioncamera.utils.harness.ts纯公共工具如覆盖所有输出 / 界面朝向组合的getUIRotation(...)visioncamera.coordinates.harness.tsxFrame.convertFramePointToCameraPoint/convertCameraPointToFramePoint、PreviewView.convertViewPointToCameraPoint/convertCameraPointToViewPoint、PreviewView.createMeteringPoint、convertScannedObjectCoordinatesToViewCoordinates、端到端 Frame → Camera → View 往返visioncamera.nativepreviewview.harness.tsx裸NativePreviewView生命周期、布局敏感预览回归覆盖、resizeMode、AndroidimplementationMode、手势控制器、多预览挂载、PreviewViewref 方法、AndroidtakeSnapshot()尺寸visioncamera.camera-view.harness.tsx高层Camera预览生命周期、photo 输出集成、控制器 props、原生手势、CameraRef方法、isActive、挂载 / 卸载 / 替换行为选择与你测试内容最匹配的文件。如果要复现的 bug 跨越多个输出放入失败最核心的那个文件。如果都不合适新建visioncamera.domain.harness.ts——Jest 会自动拾取所有匹配__tests__/**/*.harness.{ts,tsx}的文件。测试编写规范九条硬性契约这套测试的契约刻意严格目的是让测试读起来和用户写的VisionCamera业务代码完全一致——贡献者和 LLM 无需学习框架专用 helper 就能直接插入复现代码。1. 直接使用VisionCameraAPI禁止抽 helper每个测试都要从VisionCamera开始内联地端到端构建会话。不要抽createSession()或configureAndStart()之类的 helper——测试里的 API 应该和用户在 App 中写的一模一样。以下代码来自 visioncamera.photo.harness.ts是一个完整的内存 JPEG 拍摄测试it(captures a JPEG Photo in-memory, async () { const session await VisionCamera.createCameraSession(false) const photoOutput VisionCamera.createPhotoOutput({ targetResolution: CommonResolutions.FHD_4_3, containerFormat: jpeg, quality: 0.9, qualityPrioritization: balanced, }) await session.configure([ { input: backDevice, outputs: [{ output: photoOutput, mirrorMode: auto }], constraints: [], }, ]) await session.start() const photo await photoOutput.capturePhoto( { flashMode: off, enableShutterSound: false }, {}, ) expect(photo.width).toBeGreaterThan(0) expect(photo.containerFormat).toBe(jpeg) photo.dispose() await session.stop() })beforeAll可以缓存平凡的 API 结果例如CameraDeviceFactory和默认的后置 / 前置CameraDevice但不能包装任何相机会话搭建。每个it块拥有自己独立的session、photoOutput等以尽可能原子地运行。每个原子测试必须正确销毁非平凡对象避免在测试之间泄漏硬件状态——最重要的是始终stop()甚至dispose()一个CameraSession。2. 硬需求 vs 软需求不同相机的硬件能力不同硬需求失败是真实 bug软特性缺失属于设备限制不应让测试失败。硬需求——用expect(...)检查使测试失败。例如存在后置摄像头photo 输出产出的照片width 0session.configure为每个 connection 返回一个 controller或某个 API 契约如约成立。软需求——由匹配的能力标志门控不支持时用context.skip(what: reason)。Harness 会将其报告为带原因的跳过测试出现在运行摘要和 JUnit 输出中。不要用console.log(...)return来掩盖运行时相机能力缺口。it(resolves photoHDR: true when the device supports photo HDR, async (context) { if (!backDevice.supportsPhotoHDR) { return context.skip(photoHDR: not supported on this device) } // hard-assert HDR behavior from here on })当可空值需要随后收窄时使用上述 guard 形式而非context.skip(condition, reason)并保留return让 TypeScript 理解后续代码只在能力存在时执行。如果矩阵中只有一个可选用例不受支持把它拆成独立的it(...)让始终支持的用例照常运行、可选用例报告为 skipped。只能从it(...)内部或有意跳过整个测试的代码中调用context.skip(...)不要藏进共享 helper 里做可选子断言——它会中止整个it(...)。跨平台测试若有仅 Android 或仅 iOS 的断言应拆成带各自平台跳过的独立it(...)。能力标志分布在三处CameraDevicehasFlash、hasTorch、supportsFocusMetering、supportsExposureBias、supportsPhotoHDR、supportsFPS(n)、supportsVideoStabilizationMode(cinematic)等CameraControllerminISO、maxISO、minExposureDuration等VisionCamerasupportsMultiCamSessions。务必使用它们。不要用临时的 try/catch 包裹某个操作来静默跳过——如果无法提前查询支持情况把它标记为缺失 API见已知 API 缺口并用it.skip加 TODO 说明需要什么才能把它变成硬需求。3. 测试行为而不是测试类型Nitrogen 与 TypeScript 在编译期、Nitro Modules 在桥接层已经强制了类型。typeof x number或Array.isArray(devices)这类类型形状断言是纯噪音——如果数字真变成了字符串桥接层早就抛错了。应该断言那些需要相机真正干活的事实快乐路径下操作完成且不抛异常——await session.configure(...)、await photoOutput.capturePhoto(...)、await recorder.stop()能返回本身就有意义。错误路径下该抛时抛——例如在configure()之前调用session.start()、对已 dispose 的输出拍摄、请求不支持的targetResolution。用await expect(...).rejects.toThrow()。结果具有正确的语义值而非类型——拍到的Photo有width 0和height 0视频文件在磁盘上的大小 0返回的 controller 列表length connections.length。字段之间的 API 契约成立——若device.hasFlash为 false则capturePhoto({ flashMode: on })必须 reject若 connection 是mirrorMode: auto则Photo.isMirrored应反映设备前后位置。这类跨字段不变量正是类型捕获不到、真实 bug 常常藏身之处。近似数值使用 matcher 容差——对坐标、尺寸、时间戳等浮点值优先expect(actual).toBeCloseTo(expected, digits)而不是手写Math.abs(actual - expected)断言。matcher 更短、失败时能看到期望值也与坐标测试的整体风格一致。生命周期与监听器按正确顺序触发——addOnStartedListener在start()之后 resolve、addOnStoppedListener在stop()之后、录制回调在recorder.stop()之后。等待监听器不要轮询isRunning。如果某个测试把实现 stub 成throw new Error(TODO)仍然通过那说明你在测类型系统而不是相机。4. 优先回调而非轮询状态session.isRunning在 Android 上是异步更新的。等待session.addOnStartedListener(...)和addOnStoppedListener(...)配合waitUntil(() started, { timeout: 10_000 })而不是在 sleep 循环里轮询isRunning。从源码看visioncamera.session.harness.ts 正是用 test-utils.ts 中的deferred()把监听器回调接入 Promise再经withTimeout(promise, 10_000, session start)限时等待——原生错误会以 Promise rejection 形式带自身消息失败测试而不是超时。5. 不要静默吞掉错误不允许在预期成功的调用外包.catch(() undefined)或try {} catch {}。如果session.stop()可能抛异常测试就该失败——那是回归。如果某件事 100% 会抛说明是缺失特性 / 回归仍应添加测试——视上下文用it(...)或it.skip(...)。这相当于一份 TODO 清单不久后让测试变绿。6. 只在必要时 disposePhoto、Frame、Image持有大型原生缓冲区——用完立即调用.dispose()。测试中无需disposeCameraDevice、CameraController或输出JS 运行时 GC 通常会在测试间释放它们。注意在 JS 中 dispose 一个 HybridObject 后该对象即不可再用任何后续调用都会抛错——所以只在绝对必要或持有大块原生内存如Photo、Frame、Image时 dispose。7. 禁止人为setTimeout延迟测试只能等待它们真正依赖的事件session.addOnStartedListener、onRecordingFinished、帧计数器、CompletableDeferred。随机 sleep 若干毫秒让相机稳定下来会引入 flakiness 并掩盖真实回归。如果你发现自己写await sleep(500)来让它工作把它当作要修的 bug而不是要保留的补丁。唯一的例外是经过的墙钟时间本身就是被测行为的一部分。视频录制测试可以在startRecording()后短暂 sleep因为确实需要 recorder 产出非空片段、收集统计、随时间练习暂停 / 恢复或观察cancelRecording()之后不再发出onRecordingFinished。保持这些 sleep 短小、局限于录制阶段并从周围测试中能明显看出原因。不要用 sleep 等待会话、预览、帧或监听器生命周期状态。8. 平台守卫纯 iOS 特性CameraObjectOutput、continuity camera、getSupportedVideoCodecs等或纯 Android 特性CameraExtension等应以return context.skip(...: iOS only)/return context.skip(...: Android only)开头。不要用Platform.OS分支掩盖本应在两平台一致的行为差异——那应标记为 bug。如果一个行为两个平台都应支持写一个共享测试若某个平台 CI 变红保持失败可见直到平台差异被修复。Harness 测试应覆盖公共 API 承诺的最宽泛行为。来自单一原生栈的 bug 报告如 AVFoundation 断言或 CameraX 异常不应成为把回归测试做成平台特定的理由——只要用户可见行为应当在所有平台一致。不要为了让测试更窄、更快或更贴近原始报告而添加平台守卫它只会掩盖另一平台的回归。拿不准时在所有 Harness 平台上运行共享行为让 CI 暴露真实的平台差异。不要用这类标志守卫已暴露运行时可用性检查的特性——例如setFocusLocked(...)可以用device.supportsManualFocus探测即使它在 Android 上原生总是false。这样未来 Android 一旦支持对焦锁定测试可自动运行。同样不要守卫因 TODO 尚未在另一平台实现、但技术上可行的特性。setFocusLocked就是这种情况——预期缺失平台上的测试保持红色直到实现这相当于维护者的任务清单。平台守卫只适用于静态确定的平台专属行为如 iOS 的CameraObjectOutput或 Android 的CameraExtension。9. 保持断言紧凑且有诊断性测试应当读起来像某个行为的小型可执行规格命名不变量而非实现细节优先使用expectedBounds、reportedBounds、roundTripped、capturedPhoto这类本地名而不是描述临时机制的变量名。用 matcher 断言替代布尔算术优先toBeCloseTo、toHaveLength、toContain、toEqual、rejects.toThrow避免内联计算布尔值再断言。这对 AWS Device Farm 的 Harness/Vitest 日志尤其重要富 matcher 保留 received 和 expected 值而 max/min 增量这类聚合检查通常只显示派生数字。对重复维度或用例用循环边、轴、格式或角点用一个小型内联数组加一个期望比四份易漂移的复制粘贴断言更清晰。把局部数学放进局部命名it块内的小函数命名一次性变换或断言如getBounds(...)没问题。不要把算术、布尔表达式、map(...)等变换直接塞进expect(...)先赋给有描述性的本地名。不要把多个事实折叠成一个计算断言如expect(a b).toBeGreaterThan(0)——应分别断言a和b让 CI 失败时能定位出错的数值。不要抽取共享 setup helper会话仍需内联构建。保持 Harness 输出安静不要给测试添加console.log。用聚焦的 matcher 断言让失败报告相关的 received 与 expected 值。运行测试本机Android 真机来自 apps/simple-camera/package.json 的脚本和 README 的命令组合# 1. 构建一次 debug APK cd apps/simple-camera bun run build:android # 2. 安装并授予相机 / 麦克风 / 定位权限 adb install -r android/app/build/outputs/apk/debug/app-debug.apk BUNDLE_IDcom.margelo.nitro.camera.example.simple adb shell pm grant $BUNDLE_ID android.permission.CAMERA adb shell pm grant $BUNDLE_ID android.permission.RECORD_AUDIO adb shell pm grant $BUNDLE_ID android.permission.ACCESS_FINE_LOCATION adb shell pm grant $BUNDLE_ID android.permission.ACCESS_COARSE_LOCATION # 3. 对已连接设备运行完整 harness 套件 HARNESS_ANDROID_DEVICE_MANUFACTURERmanufacturer \ HARNESS_ANDROID_DEVICE_MODELmodel \ bun run test:harness:android # 4. 或只跑一个文件 HARNESS_ANDROID_DEVICE_MANUFACTURERmanufacturer \ HARNESS_ANDROID_DEVICE_MODELmodel \ bun run test:harness:android -- --testPathPatternsphotoHARNESS_ANDROID_DEVICE_MANUFACTURER/HARNESS_ANDROID_DEVICE_MODEL来自adb shell getprop ro.product.manufacturer/ro.product.model。在 AWS Device Farm 上由工作流自动设置。权限每次安装只授予一次。如果用adb install -r重装 APK请在下次测试前重新执行pm grant行——否则第一个测试的expect(cameraPermissionStatus).toBe(authorized)会失败。.harness/目录由 harness 打包器自动生成且已被 gitignore可以放心删除。运行配置rn-harness.config.mjs 定义了运行器细节值得了解的要点默认 Android bundleId 为com.margelo.nitro.camera.example.simple可通过HARNESS_ANDROID_BUNDLE_ID覆盖入口为./index.js注册组件名SimpleCamera默认使用物理 Android 设备manufacturer/model设HARNESS_ANDROID_DEVICE_MODEemulator可切到Pixel_API_35模拟器API 35可经HARNESS_ANDROID_EMULATOR/HARNESS_ANDROID_API_LEVEL调整iOS 侧 CI 下使用物理设备HARNESS_IOS_DEVICE_ID本地默认模拟器iPhone 16 Pro/ iOS 18.5超时按 CI 环境自动放宽CI 下 bundle 启动超时 90s、桥接超时 120s本地分别为 15s / 45s最大 App 重启次数 CI 为 4、本地为 2开启了detectNativeCrashes、resetEnvironmentBetweenTestFiles、forwardClientLogs与permissions。已知 API 缺口 / 当前跳过的测试少数测试已编写但被it.skip因为 VisionCamera API 尚未暴露它们所需的前置条件。每个 skip 在文件中都带 TODO 指向需要先落地的能力。当前包括Photo 容器格式支持——HEIC 和 DNG 拍摄在某些设备上可用、另一些失败但当前没有CameraDevice.supportedPhotoContainerFormats。这些测试it.skip并带 TODO直到 API 落地一旦存在它们会变成由标志门控的软需求。Android 上的initialZoom/initialExposureBias——applyInitialConfig在configure()时运行早于 CameraX 的 LifecycleOwner 到达 STARTED。CameraControl.setExposureCompensationIndex在该状态下静默失败。相关测试保持it.skip直到初始配置的应用时机移到 CameraX 能接受的点。Android 上的enablePreviewSizedOutputBuffers——该标志当前未被HybridFrameOutput.kt采纳源码注释为TODO: enablePreviewSizedOutputBuffers is not taken into account here.。Android 上的onFrameDropped——HybridFrameOutput.setOnFrameDroppedCallback当前是空操作TODO: CameraX does not have a way to figure out if a Frame has been dropped or not.。如果遇到另一个因 API 缺失而无法写测试的情况用it.skip加 TODO 说明前置条件添加测试——这样 API 落地时我们已经知道该翻转启用哪些测试。CI 集成与调试Harness 测试在每次触及本目录、VisionCamera 库或 harness 工作流配置的 push 和 PR 上运行见 .github/workflows/harness-aws-device.yml 与 .github/workflows/harness-android-emulator.yml。AWS Device Farm 运行是事实来源真机、真 SoC、真实相机管线。模拟器运行是尽力而为可能跳过依赖硬件的测试。CI 侧的 Android 流程可参考 run-harness-android-ci.sh它等待模拟器、安装 APK、验证应用启动且不立即崩溃60s 启动超时失败时导出 crash 日志再以硬超时默认 720s可经HARNESS_ANDROID_TEST_TIMEOUT_SECONDS调整运行bun run test:harness:android超时即中止并失败。PR 的 CI 失败时最快的调试路径从失败工作流下载harness output log工件它包含每个测试的完整 JS 控制台输出检查 Harness/JUnit 的 skipped 测试摘要看哪些软需求被跳过——skip 原因会告诉你测试设备缺什么搜索FAIL找出哪些it块失败及其堆栈在 IDE 的 JUnit 查看器中打开 JUnit XML 工件获得结构化摘要。理想情况下在真机上运行 Harness 测试并流式查看原生日志Android 用adb logcat以理解某些失败或原生崩溃。高层组件测试Camera、useCamera()等高层组件测试在 API 表面涉及 React 渲染、布局或组件便利行为时与命令式套件同目录存放。保持聚焦渲染复现行为的最小组件树、等待真实生命周期事件、通过公共 ref 或回调断言。详细的预览渲染、布局、快照、resize-mode 与 implementation-mode 覆盖属于 visioncamera.nativepreviewview.harness.tsxvisioncamera.camera-view.harness.tsx 应专注高层Camera包装isActive、生命周期回调、ref 暴露、输出接线、高层手势 props、React 挂载 / 卸载行为。目标是命令式 API 测试覆盖一切高层组件测试只覆盖它们的抽象层或基础特性底层使用同一套命令式 API——不要重复命令式 API 的整套测试。例如无需同时在命令式测试和Camera/useCamera()高层测试中验证CameraVideoOutput在maxFileSize达到后正确停止录制这类具体测试只放在命令式 API 测试里更聚焦、更易在 CI 调试高层测试保持高层——确保 Camera 能启动、React 生命周期 / 卸载 / 重挂载正常、Camera正确渲染、渲染新 session 时能拆除旧会话并重新开始、isActive生效、outputs{[...]}数组更新时挂接输出、测试 ref 方法等。对于布局回归优先几何与原生 ref 断言而非黄金截图——Device Farm 的相机画面不是稳定的视觉基准。AWS Device Farm 的相机传感器常被胶带盖住不会显示明亮视觉内容但也不是全黑——是带噪点的灰调有时呈红褐色像手指按在镜头上。做视觉测试时确保不是纯黑、纯白或其他纯色——相机预览应该是黑白之间的噪点。这有助于区分真正的相机流与 React 中纯黑 / 纯白背景视图或resizeModecontain的填充 / 留白。仍然允许挂载原生 Hybrid 视图来练习其 ref 方法。某些命令式 API如PreviewView.convertViewPointToCameraPoint、PreviewView.createMeteringPoint只能通过已挂载、已布局的视图触达。这些测试可以render(NativePreviewView ... /)、经hybridRef取 ref 后直接调用方法——参见 visioncamera.nativepreviewview.harness.tsx 和 visioncamera.coordinates.harness.tsx 中的命令式模式。后者的坐标往返测试Frame → Camera → Frame在 worklet 线程内对帧中心与四角做两次转换并scheduleOnRN回主线程比对正是最小可复现 硬断言的范本。【免费下载链接】react-native-vision-camera A powerful, high-performance React Native Camera library.项目地址: https://gitcode.com/GitHub_Trending/re/react-native-vision-camera创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价