What are traits in Swift Testing, and how do you skip, tag, bound and annotate tests with them?
Predict first
Which of these tests run: .disabled, .enabled(if: false), and one with tags, a time limit and a bug link?
// === Package.swift ===
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "Lib", targets: [.target(name: "Lib"), .testTarget(name: "LibTests", dependencies: ["Lib"])])
// === Sources/Lib/Lib.swift ===
public func square(_ x: Int) -> Int { x * x }
// === Tests/LibTests/T.swift ===
import Testing
@testable import Lib
extension Tag { @Tag static var slow: Self }
let hasNetwork = false
@Test(.disabled("flaky on CI")) func disabledOne() { #expect(Bool(false)) }
@Test(.enabled(if: hasNetwork)) func conditional() { #expect(Bool(false)) }
@Test(.tags(.slow), .timeLimit(.minutes(1)), .bug("https://example.com/bug/7", "tracked")) func tagged() { #expect(square(3) == 9) }
@Suite(.tags(.slow)) struct Inherits { @Test func inner() { #expect(true) } }
- The disabled and conditional tests are skipped; the tagged test and the suite test pass
- All four tests run
- The tagged test is skipped because of its tag
Mental model. Traits are sticky notes on the test: “skip while Safari is broken”, “this belongs to the slow group”, “abandon after a minute”. The test body stays the same.
A trait is a value passed to @Test or @Suite that changes how a test is run or reported. .disabled("reason") skips the test and records the reason (the check prints it as skipped, and it still compiles, so it does not rot like commented-out code). .enabled(if: condition) skips it when the condition is false, which is the right way to guard tests that need a capability (a feature flag, an OS version, a network). .tags(...) attaches tags you declare with extension Tag { @Tag static var slow: Self }, which you can then filter on in Xcode test plans or the test navigator; a trait on a suite applies to all tests in it. .timeLimit(.minutes(1)) fails a test that runs too long, and its granularity is whole minutes, so it is a backstop for hangs, not a performance assertion. .bug(url, "title") links the test to an issue so reports show it, and .serialized is the trait for ordering (next questions).
Traits compose and are inherited from the enclosing suite, which keeps conditions in one place. Use .disabled only with a reason and a tracking bug, and prefer withKnownIssue when the test should still run and be noticed if the bug gets fixed. Tags are better than naming conventions for choosing tiers (fast, slow, network). You can write your own traits by conforming to TestTrait and SuiteTrait (scoping is covered separately).
Disabled and false-conditional tests are skipped with their reasons; tags, limits and bug links do not change execution.
Common traps
- Disabling a flaky test with no bug attached. It stays disabled forever; always link a bug or an owner.
- Using
.timeLimitas a speed check. The minimum unit is a minute; it only catches hangs.
They might follow up with
- How do I run only the slow tag? Filter by tag in an Xcode test plan or the navigator; the command-line name filters match test names.
- Does a skipped test count as passed? No, it is reported as skipped.
Related questions
- 22.8.1 With @Test(arguments:), how are several argument collections combined, and how is a failure reported?
- 22.8.3 How do you assert thrown errors in Swift Testing, and what do withKnownIssue and Issue.record add?
- 22.2.1 How do XCTest and Swift Testing differ, and when do you still use XCTest?
- 22.7.1 Why do tests flake, how do you find and fix them, and what makes a CI test run hermetic?
← With @Test(arguments:), how are several argument collections… How do you assert thrown errors in Swift Testing, and what do… →