
Baguette架构解析App/Domain/Infrastructure三层设计如何让私有API代码可测试【免费下载链接】baguetteHeadless control for Apples Simulators — 3D models, taps, swipes, multi-finger gestures, 60 fps streaming, and a multi-device farm项目地址: https://gitcode.com/gh_mirrors/baguette/baguetteBaguette 是一款针对 Apple iOS 模拟器的无头控制工具headless control支持 3D 设备模型、点击滑动、多指手势、60fps 屏幕流和多设备设备农场。它大量调用 Apple 未公开的私有 API如 SimulatorKit 的 IndigoHID 管线而这类代码通常是单元测试的重灾区——没法 mock、没法离线跑、换个 Xcode 版本就崩。本文完整拆解 Baguette 的 App / Domain / Infrastructure 三层架构带你快速看懂它是如何用不到 20 行接口定义把私有 API 代码变成几秒跑完、无需启动模拟器的纯测试。为什么私有 API 代码很难测试先理解痛点。Baguette 的核心能力——在已启动的 iOS 26 模拟器里注入真实触摸——依赖 Xcode 26 中一个 9 参数签名的私有函数IndigoHIDMessageForMouseNSEvent它通过 ObjC 运行时 dlsym动态解析符号而不是编译期链接。这意味着不能importPackage.swift 刻意不链接 SimulatorKit / CoreSimulator避免LC_LOAD_DYLIB在main()前就因 Xcode 路径不同而失败不能起真模拟器每次启动要数秒CI 上还要抢 Xcode 授权线程敏感该函数读取 AppKit 的线程局部状态从 NIO 事件循环线程调用会构建出畸形消息并被模拟器静默丢弃。如果测试必须驱动真实的模拟器测试就慢、脆、不可移植。Baguette 的解法不是想办法把私有调用隔离在某个测试桩里而是把架构本身按是否碰私有 API切开。三层架构总览依赖只能向内Baguette 的源码位于 Sources/Baguette/划分为三层见 docs/ARCHITECTURE.md层目录职责依赖规则AppApp/CLI 命令分发 用例编排可依赖 Domain InfrastructureInfrastructureInfrastructure/Mockable协议的具体实现只能依赖 DomainDomainDomain/纯 Swift 值类型 边界协议不依赖任何东西仅 Foundation/IOSurface依赖方向严格单向App → Domain Infrastructure → Domain。所有私有 API 调用都被关在 Infrastructure 一层的门后面这是后面一切可测试性的前提。三个目录内部还按限界上下文bounded context二次切分——Simulator/、Input/、Screen/、Stream/、Chrome/等子文件夹在两层之间一一镜像Tests/BaguetteTests/ 也镜像同样的切分。一个功能在仓库里只有一个家找代码和找测试都不用跳来跳去。Domain 层纯值类型 Mockable协议Domain 层是架构的心脏它只回答一个问题一次输入长什么样、该产生什么行为完全不关心行为由谁执行。以点击手势为例Domain/Input/Tap.swift 定义了一个结构体struct Tap: Gesture, Equatable { let at: Point; let size: Size; let duration: Double func execute(on input: any Input) - Bool { input.tap(at: at, size: size, duration: duration, edge: edge) } }Tap是一个值类型可从 JSON 解析parse(_:)、可比较Equatable、执行时只调用注入的Input抽象。而这个Input抽象本身也定义在 Domain 层——Domain/Input/Input.swiftMockable protocol Input: Sendable { func tap(at point: Point, size: Size, duration: Double, edge: DeviceEdge?) - Bool func swipe(from start: Point, to end: Point, size: Size, duration: Double) - Bool // touch1 / touch2 / button / key / scroll / twoFingerPath … }两个细节是精华所在协议里没有任何 Apple 私有类型。Point、Size、DeviceEdge全是 Domain 自有的普通类型Domain/Common/CoordinateTypes.swift。坐标系是设备点数而非私有框架的坐标空间抽象边界上不存在任何测试环境无法满足的依赖。Mockable注解来自 Package.swift 引入的 Mockable 库会让编译器在调试构建时自动生成MockInput类测试直接拿来就用手写 mock 代码为零。整个 Domain 层没有一行 ObjC 调用没有一行dlsym编译产物甚至可以脱离 Xcode 模拟器环境独立验证。Infrastructure 层私有 API 的适配器Infrastructure 层持有全部脏活。以输入为例Infrastructure/Input/IndigoHIDInput.swift 中的IndigoHIDInput实现了 Domain 的Input协议final class IndigoHIDInput: Input, unchecked Sendable { // 通过 ObjC 运行时解析 9 参数版 IndigoHIDMessageForMouseNSEvent // 将 Domain 的 (at:size:duration:) 翻译成私有 HID 消息并投递 }注意方向是私有 API 适配 Domain 协议而不是 Domain 适配私有 API。7 个以上的文件IOHIDDigitizerDispatch.swift、IndigoHIDMessage.swift 等共同把字节补丁、digitizer 目标0x32、服务预热这些 iOS 26 细节全部封装在这一层。同一上下文下其他端口的分工摘自 docs/ARCHITECTURE.md上下文端口Domain具体实现Infrastructure包裹的私有/系统机制SimulatorSimulatorsCoreSimulatorsCoreSimulator 私有类经 ObjC 运行时ScreenScreenSimulatorKitScreenSimDevice.io帧缓冲回调InputInputIndigoHIDInput9 参数 IndigoHID 管线StreamStreamMJPEGStream/AVCCStreamVideoToolbox 编码ChromeChromesLiveChromes系统 profile.plist PDF 栅格化App 层薄编排两条入口共享一条路径App 层只放 ArgumentParser 命令和把请求变成 Domain 调用的编排代码本身不含业务逻辑。两条消费入口——stdin 子进程供宿主编程器调用和 WebSocketbaguette serve自带 Web UI——最终汇合到同一个编排器 GestureDispatcherstdin JSON / WS 文本帧 │ ▼ GestureDispatcher ──► GestureRegistry.standard.parse(dict) → Tap │ ▼ Tap.execute(on: input) ──► input.tap(...) │ ▼ IndigoHIDInput唯一碰私有 API 的地方──► 已启动的 iOS 模拟器GestureDispatcher是纯函数式的进一行 JSON出一行 JSON 应答{ok:true}/{ok:false,error:...}。它持有any Input生产环境注入IndigoHIDInput测试环境注入MockInput——App 层的逻辑因此一行不用改就能被完整验证。私有 API 代码是如何被测试的这是三层设计的回报兑现处。看 Tests/BaguetteTests/App/GestureDispatcherTests.swiftTest func dispatches a valid tap and returns oktrue() { let input MockInput() given(input).tap(at: .any, size: .any, duration: .any, edge: .any) .willReturn(true) let dispatcher GestureDispatcher(input: input) let ack dispatcher.dispatch( line: #{type:tap,x:1,y:2,width:100,height:200}#) #expect(ack #{ok:true}#) }MockInput是Mockable宏自动生成的——没有手写 stub没有启动模拟器没有 Xcode 授权弹窗。测试断言的是返回的状态Chicago-school 状态断言而非交互记录整个测试套件几秒内跑完且不需要已启动的模拟器docs/ARCHITECTURE.md。更妙的是连 Infrastructure 层自己也能被测试。例如 IndigoHIDTouchTargetTests.swift 直接对IOHIDDigitizerDispatch.patch打的字节缓冲区做断言——IndigoHIDInput构造时注入的DeviceHost同样是Mockable端口MockDeviceHost()所以即便被测对象是最靠近私有 API 的代码也不需要一个真实模拟器Test func IndigoHIDInput defaults touch target to phone digitizer() { let host MockDeviceHost() let input IndigoHIDInput(udid: ghost, host: host) #expect(input.touchTarget 0x32) }还有两个工程细节值得新手记住mock 代码不进生产包。Package.swift 中MOCKING编译标志仅在.debug配置开启release 构建里完全不含任何 mock 代码分层测试金字塔。纯解析器DeviceChrome、GestureRegistry喂 JSON 断言值每个手势测解析 execute 调对了端口方法聚合语义running/available/listJSON驱动 mock 聚合断言状态组合型实现如LiveChromes则 mock 掉它的协作者ChromeStorePDFRasterizer验证缓存命中/失败路径。扩展一个新功能要动几处这种结构还带来开闭原则OCP红利docs/ARCHITECTURE.md 给出的答案很干脆加一种手势 Domain/Input/ 下一个新的Gesture结构体 GestureRegistry.standard里注册一行。调用方stdin、WebSocket、serve页面零改动。加一种流格式 Infrastructure/Stream/ 下一个Stream实现 StreamFormat.makeStream加一个 case。新代码全部落在外层既有逻辑不被触碰——这正是三层最实际的收益。小结这套模式能抄走什么Baguette 的做法可以浓缩成四句话适用于任何要调用私有/难 mock 依赖系统框架、C 库、进程间通信的项目按是否触碰难依赖切层而不是按技术类型model/view/controller切协议定义在外层Domain实现细节关在里层Infrastructure且协议签名里不出现任何私有类型用Mockable之类的代码生成消灭手写 stubmock 只存在于 debug 构建所有入口CLI、HTTP、WebSocket汇合到同一个纯函数编排器让最上层也变成纯逻辑可测试。想深入源码建议从 docs/ARCHITECTURE.md 读起配合 docs/commands.md全部命令行参数、docs/wire.md手势线格式和 docs/serve.mdserve全部路由手势管线的字节级注释在 Sources/Baguette/Infrastructure/Input/IndigoHIDInput.swift 中写得相当详尽。【免费下载链接】baguetteHeadless control for Apples Simulators — 3D models, taps, swipes, multi-finger gestures, 60 fps streaming, and a multi-device farm项目地址: https://gitcode.com/gh_mirrors/baguette/baguette创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考