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
)
Add tests
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
@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
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
.
Create suites by namespacing tests inside a type annotated with @Suite
. You can nest suites to mirror your module structure.
@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
).
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.
@Suite("Networking", .tags(.networking))
struct Networking {
@Test(.tags(.smoke))
func healthcheck() async throws { /* … */ }
}
Define custom tags once and reuse them:
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).
Traits modify or document how a test runs. The most useful ones for concurrency:
-
.timeLimit(_:)
— fail a test that exceeds a duration.@Test(.timeLimit(.seconds(5))) func completesQuickly() async throws { /* … */ }
-
.serialized
— run enclosed tests one‑at‑a‑time (use sparingly; prefer fixing shared state).@Suite("Stateful integration", .serialized) struct Integration { /* parameterized tests, etc. */ }
-
.enabled(if:)
/.disabled()
— conditionally include tests (feature flags, OS availability).@Test(.enabled(if: FeatureFlags.search)) func searchIndexing() async throws { /* … */ }
-
.tags(_:)
— covered above.
Tip: Apply traits at a suite to inherit them for its tests.
Prefer pure functions and value types in test helpers. If state is required, isolate it.
Use actors to protect shared mutable state:**
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:
@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:
@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.
@MainActor @Test
func updatesViewModel() async {
let vm = ViewModel()
await vm.load()
#expect(vm.isLoaded)
}
- 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; writeasync
tests.
-
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)
- Run everything:
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.
Require then continue
@Test
func requireExample() {
let value = #require(Int("42"))
#expect(value == 42)
}
Parameterized with zip
(pairwise)
@Test("status mapping", arguments: zip([200, 404, 500], [true, false, false]))
func mapsStatus(code: Int, isOK: Bool) {
#expect(isOK == (code == 200))
}
Time‑boxed async
@Test(.timeLimit(.seconds(2)))
func completesUnderTwoSeconds() async throws {
_ = try await slowCall()
}
- 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.
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.