Created
October 9, 2025 15:18
-
-
Save brennanMKE/ec84033c0a71f847919f3d34716ed104 to your computer and use it in GitHub Desktop.
Revisions
-
brennanMKE created this gist
Oct 9, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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.