Packages, access control and tests

Swift · Practical · 14 min

Real projects are split into packages and tested; access control decides what each module exposes.

Swift Package Manager (SwiftPM) builds, tests and resolves dependencies. A package is a folder with a Package.swift manifest, written in Swift, listing products (libraries, executables), targets (modules, one folder each under Sources/ and Tests/) and dependencies (Git URLs with version requirements). Common commands: swift package init, swift build, swift run, swift test. Xcode opens packages directly.

Each target is a module, and access control is defined relative to modules and files:

  • private — enclosing declaration (and its extensions in the same file).
  • fileprivate — the current file.
  • internal — the whole module (the default).
  • package — any module in the same package (Swift 5.9+).
  • public — visible to importers, but classes can't be subclassed and methods overridden outside the module.
  • open — public plus subclassable and overridable.

Design small public APIs and keep everything else internal.

For tests, Swift Testing (bundled since Swift 6) uses import Testing, functions marked @Test, and the #expect(...) and #require(...) macros, which show the values of sub-expressions on failure. @Test(arguments:) runs a test once per input. Tests run in parallel by default. @testable import MyLib gives tests access to internal declarations. XCTest still works and can live alongside it.

Example

// swift-tools-version: 6.0   (Package.swift; this line comes first)
import PackageDescription
let package = Package(
    name: "MathKit",
    products: [.library(name: "MathKit", targets: ["MathKit"])],
    targets: [
        .target(name: "MathKit"),
        .testTarget(name: "MathKitTests", dependencies: ["MathKit"]),
    ]
)
// Sources/MathKit/Math.swift
public func isPrime(_ n: Int) -> Bool {
    guard n > 1 else { return false }
    var d = 2
    while d * d <= n { if n % d == 0 { return false }; d += 1 }
    return true
}
// Tests/MathKitTests/MathTests.swift
import Testing
@testable import MathKit

@Test(arguments: [2, 3, 13, 97])
func primes(n: Int) { #expect(isPrime(n)) }

@Test func nonPrimes() { #expect(!isPrime(1) && !isPrime(91)) }

Remember

  • A package has products, targets (modules) and dependencies in Package.swift
  • Default access is internal; public/open export across modules
  • Swift Testing: @Test, #expect, parameterized arguments:

Watch out for

  • Forgetting that public classes can't be subclassed outside the module unless marked open.

Check yourself

  1. What is the default access level of a declaration with no modifier?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.swift.org/documentation/package-manager/