news 2026/10/29 18:21:02

Baguette架构解析:App/Domain/Infrastructure三层设计如何让私有API代码可测试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Baguette架构解析:App/Domain/Infrastructure三层设计如何让私有API代码可测试

Baguette架构解析:App/Domain/Infrastructure三层设计如何让私有API代码可测试

【免费下载链接】baguetteHeadless control for Apple's Simulators — 3D models, taps, swipes, multi-finger gestures, 60 fps streaming, and a multi-device farm项目地址: https://gitcode.com/gh_mirrors/baguette/baguette

Baguette 是一款针对 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动态解析符号,而不是编译期链接。这意味着:

  • 不能import:Package.swift 刻意不链接 SimulatorKit / CoreSimulator,避免LC_LOAD_DYLIB在main()前就因 Xcode 路径不同而失败;
  • 不能起真模拟器:每次启动要数秒,CI 上还要抢 Xcode 授权;
  • 线程敏感:该函数读取 AppKit 的线程局部状态,从 NIO 事件循环线程调用会构建出畸形消息并被模拟器静默丢弃。

如果测试必须驱动真实的模拟器,测试就慢、脆、不可移植。Baguette 的解法不是"想办法把私有调用隔离在某个测试桩里",而是把架构本身按"是否碰私有 API"切开。

三层架构总览:依赖只能向内

Baguette 的源码位于 Sources/Baguette/,划分为三层(见 docs/ARCHITECTURE.md):

层目录职责依赖规则
AppApp/CLI 命令分发 + 用例编排可依赖 Domain + Infrastructure
InfrastructureInfrastructure/@Mockable协议的具体实现只能依赖 Domain
DomainDomain/纯 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.swift:

@Mockable 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 … }

两个细节是精华所在:

  1. 协议里没有任何 Apple 私有类型。Point、Size、DeviceEdge全是 Domain 自有的普通类型(Domain/Common/CoordinateTypes.swift)。坐标系是"设备点数"而非私有框架的坐标空间,抽象边界上不存在任何测试环境无法满足的依赖。
  2. @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 子进程(供宿主编程器调用)和 WebSocket(baguette serve自带 Web UI)——最终汇合到同一个编排器 GestureDispatcher:

stdin 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.swift:

@Test func `dispatches a valid tap and returns ok=true`() { 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 掉它的协作者(ChromeStore+PDFRasterizer)验证缓存命中/失败路径。

扩展一个新功能要动几处?

这种结构还带来"开闭原则"(OCP)红利,docs/ARCHITECTURE.md 给出的答案很干脆:

  • 加一种手势= Domain/Input/ 下一个新的Gesture结构体 +GestureRegistry.standard里注册一行。调用方(stdin、WebSocket、serve页面)零改动。
  • 加一种流格式= Infrastructure/Stream/ 下一个Stream实现 +StreamFormat.makeStream加一个 case。

新代码全部落在外层,既有逻辑不被触碰——这正是"三层"最实际的收益。

小结:这套模式能抄走什么?

Baguette 的做法可以浓缩成四句话,适用于任何要调用私有/难 mock 依赖(系统框架、C 库、进程间通信)的项目:

  1. 按"是否触碰难依赖"切层,而不是按技术类型(model/view/controller)切;
  2. 协议定义在外层(Domain),实现细节关在里层(Infrastructure),且协议签名里不出现任何私有类型;
  3. 用@Mockable之类的代码生成消灭手写 stub,mock 只存在于 debug 构建;
  4. 所有入口(CLI、HTTP、WebSocket)汇合到同一个纯函数编排器,让"最上层"也变成纯逻辑可测试。

想深入源码,建议从 docs/ARCHITECTURE.md 读起,配合 docs/commands.md(全部命令行参数)、docs/wire.md(手势线格式)和 docs/serve.md(serve全部路由);手势管线的字节级注释在 Sources/Baguette/Infrastructure/Input/IndigoHIDInput.swift 中写得相当详尽。

【免费下载链接】baguetteHeadless control for Apple's 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 13:44:06

汇写冬天写论文 —— 图书馆暖桌前的效率时光

冬天是写论文的高峰期。十一月开题、十二月一月写初稿、春天改稿。图书馆暖气开着,窗外天黑得早,窝在座位上对着电脑,其实是个适合写论文的季节。汇写(https://www.huixielunwen.com/tool/graduationThesis)陪你走过整…

作者头像 李华
网站建设 2026/10/18 15:11:40

脚踏实地,仰望星空:阿里云栖大会上的AI棋局

日前召开的2026云栖大会,用“智以致用”四个字对AI进行了重新定义:不仅要看模型有多聪明,还要看它能不能用、能否发挥价值。在三天的大会里,阿里所摆出的棋局,一边是大模型、芯片和云计算垒起的新底座,一边…

作者头像 李华
网站建设 2026/10/19 5:51:04

2026年特种猫AI面向的创作者类型与团队使用场景

特种猫AI适合哪些团队和创作者使用?它主要面向短剧团队、MCN机构、网文漫剧工作室、自媒体创作者、广告脚本人员以及零基础用户。特种猫是重庆特种猫科技有限公司推出的网页端AI短剧、漫剧创作平台,成立于2025年,团队规模200人,按…

作者头像 李华
网站建设 2026/10/26 15:44:11

U-Boot AArch64 start.S 源码逐段解析

1. 引言 U-Boot 是嵌入式系统中最常用的引导加载程序,而 arch/arm/cpu/armv8/start.S 则是 AArch64 架构下 U-Boot(含 SPL)的复位入口汇编文件。CPU 刚上电复位后,PC 会直接跳转到 _start 标签开始执行,此时 MMU、Cache 全部关闭,代码在纯物理地址上运行。 本文将从 _s…

作者头像 李华