14-3.【模块化与包管理】如何在 SPM 中实现跨模块的测试 target 与可执行 target 共享代码?

5 阅读3分钟

在 Swift Package Manager (SPM) 中,实现跨模块共享代码的核心逻辑是:将共享代码提取到一个独立的 target,并将其作为其他 target 的依赖项。

SPM 不允许 testTarget 直接引用另一个 target 的非公开(Internal)代码,除非是在同一个包内使用 @testable import。若要跨模块(即跨 Package)或在多个可执行 target 之间共享,必须遵循以下结构设计:


1. 标准架构:提取“共享库” (Common Library)

不要尝试让两个可执行文件互相引用,而是创建一个专门的底层 target(通常命名为 CoreSharedKit)。

配置步骤:

  1. 定义共享 Target:在 Package.swift 中创建一个基础 target。
  2. 定义 Product:如果该共享代码需要被其他 Package 访问,必须将其包装在 library product 中。
  3. 添加依赖:在可执行 target 和测试 target 的 dependencies 数组中引入该共享 target。

2. 代码实现示例 (Package.swift)

假设你有一个工具类需要在主程序、插件程序以及测试套件中共同使用:

Swift

let package = Package(
    name: "MyProject",
    products: [
        .executable(name: "MainApp", targets: ["MainApp"]),
        .executable(name: "HelperTool", targets: ["HelperTool"]),
        .library(name: "AppShared", targets: ["AppShared"]) // 可选:供外部包使用
    ],
    targets: [
        // 1. 共享代码层
        .target(
            name: "AppShared",
            dependencies: []
        ),
        
        // 2. 可执行文件 A
        .executableTarget(
            name: "MainApp",
            dependencies: ["AppShared"]
        ),
        
        // 3. 可执行文件 B
        .executableTarget(
            name: "HelperTool",
            dependencies: ["AppShared"]
        ),
        
        // 4. 跨模块测试 Target
        .testTarget(
            name: "SharedLogicTests",
            dependencies: ["AppShared"] // 也可以依赖 MainApp,如果需要测试其内部逻辑
        )
    ]
)

3. 访问权限的关键点

在共享代码时,你必须注意 Swift 的访问控制级别:

  • 跨 Target 访问:在 AppShared 中定义的类、方法或属性必须标记为 publicopen。默认的 internal 级别在不同 target 之间是不可见的。

  • 测试中的特殊处理

    • 如果 testTarget 与被测 target 在同一个 Package:使用 @testable import AppShared 可以访问 internal 成员。
    • 如果 testTarget 需要测试另一个 Package 的代码:只能访问其 public 接口。

4. 高级技巧:测试辅助工具共享 (Test Utilities)

有时你不仅想共享业务代码,还想共享测试工具类(如 Mock 对象、TestCase 基类)。

千万不要把测试工具放在 testTarget 里,因为测试 target 不能被其他 target 依赖。

  • 解决方案:创建一个专门的 target 命名为 TestSupport

  • 配置

    Swift

    .target(
        name: "TestSupport",
        dependencies: ["AppShared"],
        path: "Tests/TestSupport" // 手动指定路径,使其逻辑上属于测试分类
    ),
    .testTarget(
        name: "FeatureTests",
        dependencies: ["AppShared", "TestSupport"]
    )
    

5. 常见问题:资源共享 (Resources)

如果共享的代码包含资源文件(如 JSON、图片、.xcassets):

  1. 在共享 target 中定义 resources: [...]
  2. 访问时使用 Bundle.module
  3. 注意:当可执行文件依赖该 target 时,SPM 会自动将资源打包进最终的 Bundle 中,确保运行时能够找到。

总结建议

  • 解耦:如果两个可执行文件共用超过 20% 的代码,就应该考虑拆出 Shared 模块。
  • 瘦身:保持共享模块尽可能轻量,只包含纯逻辑或数据模型,避免引入繁重的第三方依赖,否则会拖慢所有依赖它的 target 的编译速度。