Skip to content

Instantly share code, notes, and snippets.

@brennanMKE
Created October 9, 2025 15:18
Show Gist options
  • Select an option

  • Save brennanMKE/ec84033c0a71f847919f3d34716ed104 to your computer and use it in GitHub Desktop.

Select an option

Save brennanMKE/ec84033c0a71f847919f3d34716ed104 to your computer and use it in GitHub Desktop.

Revisions

  1. brennanMKE created this gist Oct 9, 2025.
    272 changes: 272 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,272 @@
    # Swift Testing: A Practical Guide

    This guide shows how to adopt the **Swift Testing** framework in Swift 6 projects, with a focus on **Swift Concurrency**. It also covers organizing tests using **suites, groups, and tags**.

    > Targets: Swift 6 language mode, Xcode 16+, Testing framework (`import Testing`)
    ---

    ## 1) Getting Started

    **Add tests**

    ```swift
    import Testing

    @Test
    func example() {
    #expect(2 + 2 == 4)
    }
    ```

    * `@Test` marks a free function or static method as a test.
    * Use `#expect` to assert. Prefer `#require` when you need to bail out early.
    * Tests run **in parallel by default**; write tests to be thread‑safe.

    **Asynchronous tests**

    ```swift
    @Test
    func fetchesUser() async throws {
    let user = try await API().user(id: 42)
    #expect(user.id == 42)
    }
    ```

    * Write `async` / `throws` just like production code.
    * Use `await` freely; no explicit expectations or semaphores are required.

    **Parameterized tests**

    ```swift
    enum Fixture { static let ids = [1, 2, 3, 5, 8] }

    @Test("user lookup succeeds", arguments: Fixture.ids)
    func looksUpUser(id: Int) async throws {
    let user = try await API().user(id: id)
    #expect(user.id == id)
    }
    ```

    * Pass a **collection** to `arguments:`; the test runs once per element.
    * Give the test a **descriptive name** as the first argument to `@Test`.

    ---

    ## 2) Organize with Suites and Groups

    Create **suites** by namespacing tests inside a type annotated with `@Suite`. You can nest suites to mirror your module structure.

    ```swift
    @Suite("API")
    struct APITests {
    @Test func decode() { /**/ }

    @Suite("Auth")
    struct Auth {
    @Test func signIn() async throws { /**/ }
    }
    }
    ```

    Use **groups** by naming suites meaningfully and nesting where it matches your domain (e.g., `Networking`, `Database`, `UI`).

    ---

    ## 3) Tagging Tests (categorize and filter)

    Tags let you slice test runs (e.g., `smoke`, `networking`, `ui`, `linuxOnly`). Apply tags with the `.tags` trait on a test or an entire suite.

    ```swift
    @Suite("Networking", .tags(.networking))
    struct Networking {
    @Test(.tags(.smoke))
    func healthcheck() async throws { /**/ }
    }
    ```

    Define **custom tags** once and reuse them:

    ```swift
    import Testing

    @Tag
    extension Tag {
    static var smoke: Self
    static var networking: Self
    static var ui: Self
    }
    ```

    **Filtering by tag**

    * In Xcode’s Test navigator/inspector, run tests by tag.
    * From the SwiftPM CLI, filter with arguments that match the tag (see your CI’s integration or Xcode scheme settings for tag filters).

    ---

    ## 4) Runtime Behavior with Traits

    Traits modify or document how a test runs. The most useful ones for concurrency:

    * **`.timeLimit(_:)`** — fail a test that exceeds a duration.

    ```swift
    @Test(.timeLimit(.seconds(5)))
    func completesQuickly() async throws { /**/ }
    ```

    * **`.serialized`** — run enclosed tests one‑at‑a‑time (use sparingly; prefer fixing shared state).

    ```swift
    @Suite("Stateful integration", .serialized)
    struct Integration { /* parameterized tests, etc. */ }
    ```

    * **`.enabled(if:)` / `.disabled()`** — conditionally include tests (feature flags, OS availability).

    ```swift
    @Test(.enabled(if: FeatureFlags.search))
    func searchIndexing() async throws { /**/ }
    ```

    * **`.tags(_:)`** — covered above.

    > Tip: Apply traits at a **suite** to inherit them for its tests.
    ---

    ## 5) Concurrency-Friendly Patterns

    **Prefer pure functions and value types** in test helpers. If state is required, isolate it.

    **Use actors** to protect shared mutable state:**

    ```swift
    actor TempStore {
    private var values: [String: Int] = [:]
    func set(_ k: String, _ v: Int) { values[k] = v }
    func get(_ k: String) -> Int? { values[k] }
    }

    @Test
    func actorIsolation() async {
    let store = TempStore()
    await store.set("a", 1)
    #expect(await store.get("a") == 1)
    }
    ```

    **Run independent work in parallel** with `async let` or task groups **inside** tests when validating concurrency behavior:

    ```swift
    @Test
    func parallelFetches() async throws {
    async let a = API().user(id: 1)
    async let b = API().user(id: 2)
    let (u1, u2) = try await (a, b)
    #expect(u1.id != u2.id)
    }
    ```

    **Cancellation** — make long‑running helpers cooperative and assert behavior:

    ```swift
    @Test
    func cancelsWork() async throws {
    let task = Task { try await Work().run() }
    task.cancel()
    do {
    _ = try await task.value
    #expect(Bool(false), "expected CancellationError")
    } catch is CancellationError { /* success */ }
    }
    ```

    **Main‑actor code** — mark tests or helpers with `@MainActor` when interacting with UI or main‑thread–only APIs.

    ```swift
    @MainActor @Test
    func updatesViewModel() async {
    let vm = ViewModel()
    await vm.load()
    #expect(vm.isLoaded)
    }
    ```

    ---

    ## 6) Migrating from XCTest (quick notes)

    * You can keep XCTest and Swift Testing **side‑by‑side**.
    * Replace `XCTestCase` subclasses with free functions or suites using `@Suite`.
    * Replace `XCTAssert…` with `#expect` / `#require`.
    * For async code, remove `XCTestExpectation` boilerplate; write `async` tests.

    ---

    ## 7) CI & Command Line

    * Tests run with the SwiftPM CLI and in Xcode. Examples:

    * **Run everything:** `swift test`
    * **Run a suite or a test by name:** use Xcode’s navigator or CLI filters
    * **Filter by tag:** configure tags in your scheme or CI runner (per your tool’s support)

    **Parallelism** is default; keep tests isolated and idempotent. For unavoidable shared resources (file system, ports), scope via temp directories and unique identifiers, or fall back to `.serialized` temporarily while refactoring.

    ---

    ## 8) Handy Patterns & Snippets

    **Require then continue**

    ```swift
    @Test
    func requireExample() {
    let value = #require(Int("42"))
    #expect(value == 42)
    }
    ```

    **Parameterized with `zip`** (pairwise)

    ```swift
    @Test("status mapping", arguments: zip([200, 404, 500], [true, false, false]))
    func mapsStatus(code: Int, isOK: Bool) {
    #expect(isOK == (code == 200))
    }
    ```

    **Time‑boxed async**

    ```swift
    @Test(.timeLimit(.seconds(2)))
    func completesUnderTwoSeconds() async throws {
    _ = try await slowCall()
    }
    ```

    ---

    ## 9) Recommendations (Swift 6 mode)

    * Enable **strict concurrency** in build settings and make your test helpers `Sendable` where applicable.
    * Prefer `async let`, task groups, and actors over shared mutable state.
    * Tag **slow**, **networked**, and **flaky** tests; keep your default CI job fast by excluding these tags.
    * Use `.timeLimit` to catch hangs early.
    * Use `.serialized` only as a temporary crutch for legacy tests; refactor toward parallel‑safe helpers.

    ---

    ### Appendix: Minimal Project Layout

    ```
    MyApp/
    Sources/
    Tests/
    APITests.swift // @Suite("API"){…}
    UITests.swift // @Suite("UI", .tags(.ui))
    ```

    That’s it — you’re ready to test Swift 6 code using Swift Concurrency with clarity and speed.