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)
}@Testmarks a free function or static method as a test.- Use
#expectto assert. Prefer#requirewhen 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/throwsjust like production code. - Use
awaitfreely; 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
XCTestCasesubclasses with free functions or suites using@Suite. - Replace
XCTAssert…with#expect/#require. - For async code, remove
XCTestExpectationboilerplate; writeasynctests.
-
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
Sendablewhere 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
.timeLimitto catch hangs early. - Use
.serializedonly 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.