Advanced Test Customization with Traits
Advanced Test Customization with Traits
We have already encountered traits when using .tags() to categorize tests. However, the trait system in Swift Testing is far more extensive. Traits are the universal mechanism for annotating tests and suites to customize their runtime behavior, add metadata, and control their execution conditions. They are passed as arguments to the
@Test or @Suite macros.
Conditional Execution
Not all tests should be run all the time. Some tests might be for a feature that is currently disabled by a feature flag, while others might be temporarily failing due to a known bug or an incomplete backend. Traits provide an elegant way to manage these scenarios.
-
Enabling Tests Conditionally: The
.enabled(if:...)trait allows you to run a test only if a specific runtime condition istrue. This is perfect for testing code that is behind a feature flag.
struct FeatureFlags {
static var isNewCommentingSystemEnabled = false
}
@Test(
"Post a comment with an attachment",
.enabled(if: FeatureFlags.isNewCommentingSystemEnabled)
)
func testCommentWithAttachment() {
// This test will be skipped if the feature flag is false.
}
-
Disabling Tests: The
.disabled(...)trait allows you to temporarily skip a test. You can provide an optional string comment explaining why the test is disabled, which will be visible in the test report. This is much better than simply commenting out the test, as it keeps the test visible and serves as a reminder that it needs to be fixed.
@Test(
"Test video transcoding",
.disabled("This test is flaky and is being investigated.")
)
func testVideoTranscoding() {
// This test will be marked as "skipped" in the test run.
}
Performance and Stability
In automated testing environments, it is crucial to prevent tests from getting stuck and running indefinitely. A test that hangs can block an entire CI/CD pipeline.
-
Setting Time Limits: The
.timeLimit(...)trait allows you to specify a maximum duration for a test. If the test exceeds this limit, it will automatically fail.
@Test(
"Process large dataset",
.timeLimit(.seconds(30))
)
func testLargeDataProcessing() async throws {
// If this function takes longer than 30 seconds, the test will fail.
}
The time limit trait has specific application rules:
-
When applied to a
@Suite, the time limit is inherited by every test within that suite. -
When applied to a parameterized test (which we will cover next), the time limit applies independently to each individual run of the test with its specific arguments.
Associating Metadata
To improve traceability and context, especially in large teams, it is helpful to link tests directly to related items in external systems like bug trackers.
-
Linking to Bugs: The
.bug(...)trait allows you to associate a test with a bug report identifier or URL. This provides invaluable context for anyone who encounters a skipped or failing test, allowing them to quickly look up the relevant ticket.
@Test(
"Test currency conversion for rare currencies",
.disabled("Fails due to rounding error in backend API."),
.bug(id: "FIN-472", url: "https://your-tracker.com/FIN-472")
)
func testRareCurrencyConversion() {
//...
}
This information is displayed in the Xcode Test Report, providing a direct link to the bug tracking system.
To provide a quick summary, here is a reference table of the most common built-in traits.
| Trait | Purpose | Example Usage |
|---|---|---|
.tags(...) |
Categorize tests with semantic labels. |
.tags(.ui,.critical) |
.enabled(if:...) |
Run test only if a condition is true. | .enabled(if: FeatureFlags.isNewCheckoutEnabled) |
.disabled(...) |
Skip a test, with an optional reason. | .disabled("Backend endpoint not ready") |
.timeLimit(...) |
Fail the test if it exceeds a duration. | .timeLimit(.minutes(1)) |
.bug(...) |
Link the test to a bug report. | .bug(id: "FB-90210") |
.serialized |
Force tests in a suite to run one by one. | @Suite(.serialized) |