You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A comprehensive guide to organizing and writing tests with Apple's Swift Testing framework.
What you'll find here:
Hierarchical test organization patterns for scaling your test suite
Naming conventions and best practices that prevent common pitfalls
Quick reference cheat sheet for daily use
Complete Swift Testing API reference
Who this is for: Swift developers adopting Swift Testing, especially those working on complex projects with growing test suites that need clear organization and maintainability.
Start with this README to understand the value proposition
Skim the Quick Reference to see patterns at a glance
Read the Test Organization Guide when implementing
Reference the Swift Testing API docs as needed
Quick Example
Here's what organized Swift tests look like using Pattern A (parent has "Tests" suffix):
// File: UserManagerTests.swift
import Testing
/// Test suite for UserManager functionality.
@Suite("User Manager")internalenumUserManagerTests{}
// File: UserManagerTests+Validation.swift
import Testing
extensionUserManagerTests{
/// Input validation tests for UserManager.
@Suite("Validation Tests")internalstructValidation{
// MARK: - Test Data Setup
privatestaticletvalidUserID="user123"privatestaticletemptyUserID=""privatestaticlettooLongUserID=String(repeating:"a", count:1000)
// MARK: - User ID Validation
@Test("Validate user ID with correct format")internalfunc validateCorrectFormat(){letisValid=UserManager.validateUserID(Self.validUserID)
#expect(isValid)}@Test("Reject empty user ID")internalfunc rejectEmptyUserID(){letisValid=UserManager.validateUserID(Self.emptyUserID)
#expect(!isValid)}@Test("Reject user ID that is too long")internalfunc rejectTooLongUserID(){letisValid=UserManager.validateUserID(Self.tooLongUserID)
#expect(!isValid)}}}
Include "Tests" in EITHER parent OR child, NEVER both.
Pattern
Parent
Child
Filename
Status
Pattern A
UserManagerTests
Validation
UserManagerTests+Validation.swift
✅ Correct
Pattern B
UserManager
ValidationTests
UserManager+ValidationTests.swift
✅ Correct
❌ Avoid
UserManagerTests
ValidationTests
UserManagerTests+ValidationTests.swift
❌ Redundant
Test Helper Naming
Always extend the production type, not the test enum:
// ✅ Correct: Extend production type
// File: UserManager+TestHelpers.swift
extensionUserManager{internalfunc testValidate()async->Bool{...}}
// ❌ Wrong: Don't extend test enum
extensionUserManagerTests{internalfunc testValidate()async->Bool{...}}
Three Essential Patterns
1. Simple Pattern (Single Struct)
When to use: Types with < 10 tests, focused behavior, no need for categories
// File: FieldValueTests.swift
import Testing
/// Tests for FieldValue type conversions.
@Suite("Field Value")internalstructFieldValueTests{
// MARK: - Test Data Setup
privatestaticlettestString="Hello, World"privatestaticlettestNumber=42
// MARK: - String Value Tests
@Test("Create field value from string")internalfunc createFromString(){letvalue=FieldValue.string(Self.testString)
#expect(value.stringValue ==Self.testString)}@Test("Convert field value to string")internalfunc convertToString(){letvalue=FieldValue.string(Self.testString)
#expect(value.stringValue ==Self.testString)}
// MARK: - Number Value Tests
@Test("Create field value from integer")internalfunc createFromInteger(){letvalue=FieldValue.int(Self.testNumber)
#expect(value.intValue ==Self.testNumber)}}
2. Pattern A: Parent Has "Tests"
When to use: Most common pattern - matches standard test file naming conventions
// File: UserManagerTests.swift
import Testing
/// Test suite for UserManager functionality.
@Suite("User Manager")internalenumUserManagerTests{}
// File: UserManagerTests+Basic.swift
import Testing
extensionUserManagerTests{
/// Basic UserManager functionality tests.
@Suite("Basic Tests")internalstructBasic{
// MARK: - Test Data Setup
privatestaticlettestUserID="user123"privatestaticlettestConfig=UserConfig(
autoSave:true,
cacheEnabled:true)
// MARK: - Initialization Tests
@Test("Initialize with valid configuration")internalfunc initializeWithValidConfiguration(){letmanager=UserManager(config:Self.testConfig)
#expect(manager.config.autoSave)
#expect(manager.config.cacheEnabled)}@Test("Initialize with default configuration")internalfunc initializeWithDefaultConfiguration(){letmanager=UserManager()
#expect(manager.config !=nil)}
// MARK: - User Fetching Tests
@Test("Fetch user successfully")internalfunc fetchUserSuccessfully()async{letmanager=UserManager(config:Self.testConfig)letuser=await manager.fetchUser(id:Self.testUserID)
#expect(user !=nil)}}}
// File: UserManagerTests+Validation.swift
import Testing
extensionUserManagerTests{
/// UserManager validation tests.
@Suite("Validation Tests")internalstructValidation{
// MARK: - Test Data Setup
privatestaticletvalidUserID="user123"privatestaticletemptyUserID=""privatestaticlettooLongUserID=String(repeating:"a", count:1000)
// MARK: - User ID Validation
@Test("Validate user ID with correct format")internalfunc validateCorrectFormat(){letisValid=UserManager.validateUserID(Self.validUserID)
#expect(isValid)}@Test("Reject empty user ID")internalfunc rejectEmptyUserID(){letisValid=UserManager.validateUserID(Self.emptyUserID)
#expect(!isValid)}@Test("Reject user ID that is too long")internalfunc rejectTooLongUserID(){letisValid=UserManager.validateUserID(Self.tooLongUserID)
#expect(!isValid)}}}
3. Pattern B: Child Has "Tests"
When to use: Parent name should match production type exactly
// File: UserManager.swift
import Testing
/// Test suite for UserManager functionality.
@Suite("User Manager")internalenumUserManager{}
// File: UserManager+ValidationTests.swift
import Testing
extensionUserManager{
/// UserManager validation tests.
@Suite("Validation Tests")internalstructValidationTests{
// MARK: - Test Data Setup
privatestaticletvalidUserID="user123"privatestaticletemptyUserID=""
// MARK: - User ID Validation
@Test("Validate user ID with correct format")internalfunc validateCorrectFormat(){letisValid=UserManager.validateUserID(Self.validUserID)
#expect(isValid)}@Test("Reject empty user ID")internalfunc rejectEmptyUserID(){letisValid=UserManager.validateUserID(Self.emptyUserID)
#expect(!isValid)}}}
Initialization / InitializationTests - Constructor and setup tests
Validation / ValidationTests - Input validation and constraints
Errors / ErrorTests - Error handling and edge cases
Concurrent / ConcurrentTests - Concurrency and thread safety
Integration / IntegrationTests - Integration with other components
Performance / PerformanceTests - Performance and benchmarking
Decision Guide
Should I Use Simple or Hierarchical Pattern?
START: How many tests do I need?
│
├─→ Less than 10 tests
│ └─→ Use Simple Pattern (single struct)
│
└─→ More than 10 tests OR will grow over time
│
├─→ Do I need multiple test categories?
│ ├─→ YES → Use Hierarchical Pattern (enum + extensions)
│ └─→ NO → Use Simple Pattern
│
└─→ Do different tests need different test data?
├─→ YES → Use Hierarchical Pattern
└─→ NO → Use Simple Pattern
Pattern A vs Pattern B Comparison
Criteria
Pattern A (Parent has "Tests")
Pattern B (Child has "Tests")
Parent name
UserManagerTests
UserManager
Child name
Validation
ValidationTests
Filename
UserManagerTests+Validation.swift
UserManager+ValidationTests.swift
Best for
Matching test file conventions
Matching production type names
Advantage
Clear test hierarchy, shorter child names
Easy to search for all test structs
Search filter
Filter parent by "Tests"
Filter child by "Tests"
Choose Pattern A if: You want file names to immediately signal tests with "Tests" prefix
Choose Pattern B if: You want parent enum to exactly match production type name
Do I Need Test Helpers?
START: What do I need to test?
│
├─→ Async operations need simpler interface
│ └─→ Create async wrapper helpers
│
├─→ Need factory methods for test instances
│ └─→ Create factory helpers
│
├─→ Complex validation used across multiple tests
│ └─→ Create validation helpers
│
└─→ Simple operations, no repetition
└─→ No helpers needed
// File: UserManagerTests+Categories.swift
extensionUserManagerTests{structValidation{...}structErrors{...} // ❌ Should be in separate file
structConcurrent{...} // ❌ Should be in separate file
}
import XCTest
@Test("Validate user")func validateUser(){XCTAssertEqual(user.id,"123") // ❌ XCTest API
XCTAssertTrue(user.isValid) // ❌ XCTest API
XCTAssertNotNil(user.name) // ❌ XCTest API
}
✅ FIX:
import Testing
@Test("Validate user")func validateUser(){
#expect(user.id =="123") // ✅ Swift Testing
#expect(user.isValid) // ✅ Swift Testing
#expect(user.name !=nil) // ✅ Swift Testing
}
5. Mismatched File Names
❌ WRONG:
// File: UserManagerTests+ValidationTests.swift ❌ Name suggests both have "Tests"
extensionUserManagerTests{structValidation{...} // Actual child doesn't have "Tests"
}
✅ FIX:
// File: UserManagerTests+Validation.swift ✅ Matches actual names
extensionUserManagerTests{structValidation{...}}
// Basic suite
@Suite("Description")internalstructMyTests{...}
// Conditional suite (runs only on specific platform)
@Suite("iOS Tests",.enabled(if:os(iOS)))internalstructIOSTests{...}
// Tagged suite (run with: swift test --filter tag:slow)
@Suite("Performance Tests",.tags(.slow))internalstructPerformanceTests{...}
// Disabled suite
@Suite("Legacy Tests",.disabled("Deprecated functionality"))internalstructLegacyTests{...}
// Multiple attributes
@Suite("Integration Tests",.enabled(if: !DEBUG),.tags(.integration,.slow))internalstructIntegrationTests{...}
// Serial execution (tests run one at a time)
@Suite("Serial Tests",.serialized)internalstructSerialTests{...}
Test Attributes
// Basic test
@Test("Description")internalfunc testSomething(){...}
// Async test
@Test("Async operation")internalfunc testAsync()async{letresult=awaitfetchData()
#expect(result !=nil)}
// Throwing test
@Test("Validate input throws")internalfunc testThrows()throws{tryvalidateInput("test")}
// Async throwing test
@Test("Fetch data or throw")internalfunc testAsyncThrows()asyncthrows{letdata=tryawaitfetchData()
#expect(data.count >0)}
// Conditional test
@Test("macOS only",.enabled(if:os(macOS)))internalfunc testMacOSFeature(){...}
// Tagged test
@Test("Network test",.tags(.network,.slow))internalfunc testNetwork()async{...}
// Disabled test
@Test("Broken test",.disabled("Bug #12345"))internalfunc testBroken(){...}
// Bug tracking
@Test("Ice cream test",.bug("https://example.com/bug/12345","Out of sprinkles"))internalfunc testIceCream(){...}
// Time limit (fails if test takes longer)
@Test("Fast test",.timeLimit(.minutes(1)))internalfunc testFast()async{...}
// Define custom tags
extensionTag{@Tagstaticvarslow:Self@Tagstaticvarnetwork:Self@Tagstaticvarintegration:Self@Tagstaticvarunit:Self@Tagstaticvarperformance:Self}
// Use tags on suites
@Suite("Network Tests",.tags(.network,.integration))internalstructNetworkTests{...}
// Use tags on tests
@Test("API call",.tags(.network,.slow))internalfunc testAPICall()async{...}
// Run specific tags:
// swift test --filter tag:network
// swift test --filter tag:unit
// swift test --skip tag:slow
Async Testing
// Basic async test
@Test("Fetch data")internalfunc testFetchData()async{letdata=awaitfetchData()
#expect(data !=nil)}
// Async throwing
@Test("Fetch or throw")internalfunc testFetchOrThrow()asyncthrows{letdata=tryawaitfetchData()
#expect(data.count >0)}
// Concurrent operations
@Test("Concurrent fetches")internalfunc testConcurrent()async{asyncletresult1=fetch(id:1)asyncletresult2=fetch(id:2)asyncletresult3=fetch(id:3)letresults=await[result1, result2, result3]
#expect(results.allSatisfy{ $0 !=nil})}
// Actor isolation
@Test("Actor-isolated operation")internalfunc testActor()async{letactor=MyActor()letresult=awaitactor.performOperation()
#expect(result.isValid)}
// Task groups
@Test("Task group")internalfunc testTaskGroup()async{awaitwithTaskGroup(of:Int.self){ group inforiin1...10{
group.addTask{awaitfetch(id: i)}}varcount=0forawait_in group {
count +=1}
#expect(count ==10)}}
Time Limits
// Per-test time limit
@Test("Quick operation",.timeLimit(.seconds(5)))internalfunc testQuick()async{
// Must complete in 5 seconds or fail
awaitperformQuickOperation()}
// Per-suite time limit
@Suite("Fast Tests",.timeLimit(.minutes(1)))internalstructFastTests{@Test("Test 1")internalfunc test1()async{...}@Test("Test 2")internalfunc test2()async{...}
// All tests combined must complete in 1 minute
}
// Different time units
@Test("Timeout test",.timeLimit(.milliseconds(500)))@Test("Timeout test",.timeLimit(.seconds(30)))@Test("Timeout test",.timeLimit(.minutes(5)))
Serial Execution
// Suite-level serialization (all tests run one at a time)
@Suite("Serial Suite",.serialized)internalstructSerialTests{@Test("Test 1")internalfunc test1(){...}@Test("Test 2")internalfunc test2(){...}
// Tests run sequentially, not in parallel
}
// Test-level serialization (specific tests run serially)
@Test("Serial test 1",.serialized)internalfunc serialTest1(){...}@Test("Serial test 2",.serialized)internalfunc serialTest2(){...}
// Use when:
// - Tests share mutable state
// - Testing order-dependent behavior
// - Resource contention issues
// - Debugging timing issues
XCTest Migration
Quick Conversion Table
XCTest
Swift Testing
class FooTests: XCTestCase
@Suite("Foo") struct FooTests
func testSomething()
@Test("Something") func something()
XCTAssertEqual(a, b)
#expect(a == b)
XCTAssertNotEqual(a, b)
#expect(a != b)
XCTAssertTrue(condition)
#expect(condition)
XCTAssertFalse(condition)
#expect(!condition)
XCTAssertNil(value)
#expect(value == nil)
XCTAssertNotNil(value)
#expect(value != nil)
XCTAssertThrowsError
#expect(throws:)
XCTUnwrap(optional)
try #require(optional)
XCTFail("message")
Issue.record("message")
setUp() / tearDown()
init() / deinit
setUpWithError() / tearDownWithError()
init() throws / deinit
addTeardownBlock { }
Use defer in test
Migration Example
Before (XCTest):
import XCTest
classUserManagerTests:XCTestCase{varmanager:UserManager!overridefunc setUp(){
super.setUp()
manager =UserManager(config:.default)}overridefunc tearDown(){
manager =nil
super.tearDown()}func testValidateUserID(){letisValid= manager.validateUserID("user123")XCTAssertTrue(isValid,"User ID should be valid")}func testRejectEmptyUserID(){letisValid= manager.validateUserID("")XCTAssertFalse(isValid,"Empty user ID should be rejected")}func testFetchUserAsync(){letexpectation=expectation(description:"Fetch user")Task{letuser=await manager.fetchUser(id:"user123")XCTAssertNotNil(user)
expectation.fulfill()}waitForExpectations(timeout:5)}}
After (Swift Testing):
import Testing
@Suite("User Manager")internalstructUserManagerTests{
// MARK: - Test Data Setup
privatestaticlettestUserID="user123"privatestaticletemptyUserID=""privatestaticlettestConfig=UserConfig.default
// MARK: - Validation Tests
@Test("Validate user ID with correct format")internalfunc validateUserID(){letmanager=UserManager(config:Self.testConfig)letisValid= manager.validateUserID(Self.testUserID)
#expect(isValid)}@Test("Reject empty user ID")internalfunc rejectEmptyUserID(){letmanager=UserManager(config:Self.testConfig)letisValid= manager.validateUserID(Self.emptyUserID)
#expect(!isValid)}
// MARK: - Async Tests
@Test("Fetch user asynchronously")internalfunc fetchUserAsync()async{letmanager=UserManager(config:Self.testConfig)letuser=await manager.fetchUser(id:Self.testUserID)
#expect(user !=nil)}}
Key Differences
No test class inheritance - Swift Testing uses structs with @Suite, not classes inheriting from XCTestCase
No setUp/tearDown - Use init() for setup and deinit for teardown, or create instances in each test
Static test data - Use private static let for test data instead of instance properties
Native async support - Just mark test as async, no need for expectations and waitForExpectations
Simpler assertions - Use #expect() with Swift expressions instead of XCTest assertion functions
End of Quick Reference
For comprehensive coverage and detailed explanations, see:
This guide documents a hierarchical test organization pattern for Swift Testing using empty enum containers with nested struct extensions. This approach provides:
Clear hierarchy: Logical grouping of related tests
Scalability: Easy to add new test categories without file bloat
Parallel execution: Swift Testing can run suites concurrently
Discoverable structure: File organization mirrors test hierarchy
Maintainability: Isolated test categories in separate files
When to use this pattern:
Production types with multiple test categories (validation, errors, concurrency, etc.)
Test suites that will grow over time
When you need to organize tests by feature or behavior
For complex types requiring different test data per category
When to use simple patterns:
Types with a small, focused set of tests (< 10 tests)
Utility types with straightforward behavior
Tests that don't require multiple categories
Core Pattern Overview
Swift Testing supports hierarchical test organization through nested suites. This guide presents a pattern using empty enums as parent containers with struct extensions for categories, along with a critical naming convention.
Pattern A: Parent Has "Tests" Suffix
// 1. Empty enum as parent container (has "Tests")
@Suite("Parent Suite Name")internalenumParentTests{}
// 2. Extension with nested suite struct (no "Tests")
extensionParentTests{@Suite("Category Description")internalstructCategory{
// 3. Test methods
@Test("Test description")internalfunc testSomething(){
#expect(condition)}}}
File structure:
ParentTests.swift - Defines the empty enum
ParentTests+Category.swift - Extension with nested struct
Pattern B: Child Has "Tests" Suffix
// 1. Empty enum as parent container (no "Tests")
@Suite("Parent Suite Name")internalenumParent{}
// 2. Extension with nested suite struct (has "Tests")
extensionParent{@Suite("Category Description")internalstructCategoryTests{
// 3. Test methods
@Test("Test description")internalfunc testSomething(){
#expect(condition)}}}
File structure:
Parent.swift - Defines the empty enum
Parent+CategoryTests.swift - Extension with nested struct
Critical Convention: The "Tests" Suffix Rule
Never use "Tests" in both the parent enum AND child struct names.
This is the most important naming rule when using this organizational pattern. Choose one location for "Tests" and apply it consistently throughout your project.
Naming Convention Rationale
The "Tests" Suffix Rule Explained
Rule: Include "Tests" in EITHER the parent enum OR the child struct, NEVER both.
Why This Matters
Reduces Redundancy:
✅ UserManagerTests+Validation is cleaner
❌ UserManagerTests+ValidationTests is redundant
Clearer Intent: The file extension and test directory location already indicate these are tests
File: TypeNameTests+Category.swift in Tests/ directory → obviously tests
File: TypeName+CategoryTests.swift in Tests/ directory → also obviously tests
Shorter Names: Reduces cognitive load when reading test output and file lists
Consistency: Pick one pattern for your project and maintain it
Easy to search for all test structs (filter by "Tests" suffix)
Clear separation between test enums and production types
Recommendation: Choose one pattern and apply it consistently across your entire test suite. Document your choice in your project's contribution guidelines.
Step-by-Step Tutorial
Creating a New Test Suite
This tutorial demonstrates Pattern A (parent with "Tests" suffix), but the steps apply equally to Pattern B.
Create a file named after your production type with "Tests" suffix:
// File: TypeNameTests.swift
import Testing
/// Test suite for TypeName functionality.
@Suite("Type Name")internalenumTypeNameTests{}
Key points:
Empty enum (no properties or methods)
@Suite attribute with descriptive name
internal or public access level as appropriate
Import Testing framework
Step 3: Create Extension Files for Each Test Category
Create separate files for each test category:
// File: TypeNameTests+Initialization.swift
import Testing
extensionTypeNameTests{
/// Tests for TypeName initialization scenarios.
@Suite("Initialization Tests")internalstructInitialization{
// Tests go here
}}
Naming the extension file:
Format: ParentEnumName+ChildStructName.swift
Example: TypeNameTests+Initialization.swift
Child struct name has NO "Tests" suffix (parent already has it)
Common test categories:
Initialization - Constructor and setup tests
Validation - Input validation and constraints
Errors - Error handling and edge cases
Concurrent - Concurrency and thread safety
Integration - Integration with other components
Performance - Performance and benchmarking tests
Step 4: Add Test Data as Private Static Properties
extensionTypeNameTests{@Suite("Initialization Tests")internalstructInitialization{
// MARK: - Test Data Setup
/// Valid configuration for testing.
privatestaticletvalidConfig=Configuration(
timeout:30,
retryCount:3)
/// Invalid configuration for error testing.
privatestaticletinvalidConfig=Configuration(
timeout:-1,
retryCount:0)
// MARK: - Tests
@Test("Initialize with valid configuration")internalfunc initializeWithValidConfig(){letinstance=TypeName(config:Self.validConfig)
#expect(instance.config.timeout ==30)}}}
Best practices for test data:
Use private static let for immutable test data
Group related data together
Use descriptive names (validConfig, not config1)
Document complex test data with comments
Access via Self.propertyName in test methods
Step 5: Write Test Methods with @Test Attribute
extensionTypeNameTests{@Suite("Initialization Tests")internalstructInitialization{
// MARK: - Test Data Setup
privatestaticletvalidInput="test-value"
// MARK: - Basic Initialization
@Test("Initialize with valid input")internalfunc initializeWithValidInput(){letinstance=TypeName(input:Self.validInput)
#expect(instance.input ==Self.validInput)
#expect(instance.isValid)}@Test("Initialize with default configuration")internalfunc initializeWithDefaults(){letinstance=TypeName()
#expect(instance.config !=nil)}
// MARK: - Error Cases
@Test("Initialize with empty input throws error")internalfunc initializeWithEmptyInput(){
#expect(throws:ValidationError.self){
_ =tryTypeName(input:"")}}
// MARK: - Async Initialization
@Test("Initialize with async validation")internalfunc initializeWithAsyncValidation()asyncthrows{letinstance=TypeName(input:Self.validInput)letisValid=await instance.validate()
#expect(isValid)}}}
Test method guidelines:
Use descriptive names matching the @Test description
Use #expect() for assertions
Use async for async operations
Use throws when testing error conditions
Group related tests with // MARK: sections
Step 6: Add Test Helpers if Needed
If you need helper methods for testing, create a separate file:
// File: TypeName+TestHelpers.swift
extensionTypeName{
/// Simplified async validation for testing.
internalfunc testValidate()async->Bool{do{tryawaitvalidate()returntrue}catch{returnfalse}}
/// Create a test instance with default values.
internalstaticfunc testInstance(input:String="test-input")->TypeName{TypeName(input: input)}}
When to create helpers:
Async wrapper methods for easier testing
Simplified factory methods for test instances
Validation helpers used across multiple test files
Complex setup that's repeated in many tests
Where to place them:
File: TypeName+TestHelpers.swift (production type name, not test enum)
Extension on the production type, not the test enum
Use internal access level (visible to tests)
Common Pitfalls to Avoid
❌ Using "Tests" in both parent and child:
// DON'T DO THIS
internalenumTypeNameTests{}extensionTypeNameTests{structCategoryTests{...} // ❌ Redundant "Tests"
}
❌ Making test data mutable or non-static:
// DON'T DO THIS
privatevartestValue="..." // ❌ Mutable, instance property
// DO THIS
privatestaticlettestValue="..." // ✅ Immutable, static
❌ Using XCTest patterns:
// DON'T DO THIS
XCTAssertEqual(a, b) // ❌ XCTest API
// DO THIS
#expect(a == b) // ✅ Swift Testing API
❌ Mismatching file names and type names:
// File: TypeNameTests+CategoryTests.swift ❌ Name suggests both have "Tests"
extensionTypeNameTests{structCategory{...} // Actual child doesn't have "Tests"
}
// File: TypeNameTests+Category.swift ✅ Matches actual names
extensionTypeNameTests{structCategory{...}}
File Organization Conventions
Naming Patterns
CRITICAL RULE: The "Tests" suffix should appear in EITHER the parent enum OR the nested struct, but NEVER in both.
// DON'T DO THIS
internalenumTypeNameTests{}extensionTypeNameTests{structCategoryTests{...} // "Tests" appears in both
}
// File would be: TypeNameTests+CategoryTests.swift ❌ Redundant
Tests/MyLibraryTests/
└── Utils/
└── FieldValueTests.swift # Single struct: FieldValueTests
When to use simple pattern:
Small, focused types with < 10 tests
Utilities without complex behavior categories
Tests that don't need multiple categories
Test Data Management
Pattern: Private Static Properties
Test data should be defined as private static properties within each suite struct:
@Suite("Validation Tests")internalstructValidation{
// MARK: - Test Data Setup
/// Valid email address for testing.
privatestaticletvalidEmail="user@example.com"
/// Email address with invalid format.
privatestaticletinvalidEmail="not-an-email"
/// Test user configuration.
privatestaticlettestUserConfig=UserConfig(
name:"Test User",
email: validEmail,
preferences:["theme":"dark"])
// MARK: - Email Validation Tests
@Test("Validate email with correct format")internalfunc validateCorrectFormat(){letresult=EmailValidator.validate(Self.validEmail)
#expect(result.isValid)}@Test("Reject email with invalid format")internalfunc rejectInvalidFormat(){letresult=EmailValidator.validate(Self.invalidEmail)
#expect(!result.isValid)}}
Best Practices for Test Data
Do:
✅ Use descriptive names that explain the data's purpose
✅ Group related data together under the same // MARK: section
✅ Use private static let for immutable data
✅ Document complex or non-obvious test data with comments
✅ Keep test data close to the tests that use it
✅ Access via Self.propertyName in test methods
Don't:
❌ Use vague names like data1, config2
❌ Make test data mutable (var instead of let)
❌ Use instance properties (non-static)
❌ Share mutable state between tests
❌ Define test data outside the suite struct
Organizing Test Data
@Suite("Integration Tests")internalstructIntegration{
// MARK: - Test Data Setup
// MARK: User Data
privatestaticletadminUser=User(role:.admin)privatestaticletregularUser=User(role:.user)privatestaticletguestUser=User(role:.guest)
// MARK: Configurations
privatestaticletdefaultConfig=Configuration(...)privatestaticletcustomConfig=Configuration(...)
// MARK: Mock Data
privatestaticletmockResponse=HTTPResponse(...)privatestaticletmockError=NetworkError.timeout
// MARK: - Tests
// ...
}
Test Helpers Pattern
When to Create Helpers
Create test helper methods when you need:
Async wrapper methods for easier testing
Simplified interfaces for test-specific operations
Factory methods to create test instances
Validation helpers used across multiple tests
Setup/teardown helpers for complex test scenarios
Where to Place Helpers
Always extend the production type, not the test enum:
// File: TypeName+TestHelpers.swift
import Testing
extensionTypeName{
/// Simplified async validation for testing.
internalfunc testValidate()async->Bool{do{tryawaitvalidate()returntrue}catch{returnfalse}}
/// Create a test instance with default values.
internalstaticfunc testInstance(
value:String="test-value",
config:Configuration=.default
)->TypeName{TypeName(value: value, config: config)}}
Helper Method Examples
Async Wrapper Helpers
Simplify async operations for easier testing:
extensionNetworkClient{
/// Test helper: Fetch data and return success status.
internalfunc testFetch()async->Bool{do{
_ =tryawaitfetch()returntrue}catch{returnfalse}}
/// Test helper: Validate response with simple boolean result.
internalfunc testValidate(_ response:Response)async->Bool{do{tryawaitvalidate(response)returntrue}catch{returnfalse}}}
Factory Helpers
Create test instances with sensible defaults:
extensionConfiguration{
/// Create a test configuration with default values.
internalstaticvartesting:Configuration{Configuration(
timeout:30,
retryCount:3,
cacheEnabled:true)}
/// Create a minimal configuration for unit tests.
internalstaticfunc minimal()->Configuration{Configuration(
timeout:10,
retryCount:1,
cacheEnabled:false)}}
Validation Helpers
Common validation operations for tests:
extensionDataStore{
/// Test helper: Check if store contains an item.
internalfunc testContains(id:String)async->Bool{do{letitem=tryawaitretrieve(id: id)return item !=nil}catch{returnfalse}}
/// Test helper: Get count of stored items.
internalfunc testCount()async->Int{(try?awaitallItems().count)??0}}
Best Practices for Helpers
Do:
✅ Prefix helper methods with test to indicate test-only code
✅ Use internal access level (visible to test targets)
✅ Keep helpers simple and focused on testing needs
✅ Document the helper's purpose
✅ Place in separate +TestHelpers.swift file
Don't:
❌ Add helpers to production codebase (keep in test-only extensions)
❌ Make helpers public unless necessary
❌ Create helpers that duplicate production functionality
❌ Add complex business logic to helpers
Code Organization Within Test Files
MARK Sections for Logical Grouping
Use // MARK: comments to organize tests into logical sections:
extensionTypeNameTests{@Suite("Comprehensive Tests")internalstructComprehensive{
// MARK: - Test Data Setup
privatestaticletvalidInput="..."privatestaticlettestConfig=Configuration(...)
// MARK: - Initialization Tests
@Test("Initialize with valid parameters")internalfunc initializeWithValidParameters(){...}@Test("Initialize with default configuration")internalfunc initializeWithDefaultConfiguration(){...}
// MARK: - Functional Tests
@Test("Perform basic operation")internalfunc performBasicOperation()asyncthrows{...}@Test("Handle complex workflow")internalfunc handleComplexWorkflow()asyncthrows{...}
// MARK: - Error Handling Tests
@Test("Throws error for invalid input")internalfunc throwsErrorForInvalidInput(){...}@Test("Recovers from failure")internalfunc recoversFromFailure()async{...}
// MARK: - Concurrent Tests
@Test("Handles concurrent access safely")internalfunc handlesConcurrentAccessSafely()async{...}@Test("Maintains consistency under load")internalfunc maintainsConsistencyUnderLoad()async{...}
// MARK: - Edge Cases
@Test("Handles empty input")internalfunc handlesEmptyInput(){...}@Test("Handles maximum values")internalfunc handlesMaximumValues(){...}}}
Common MARK Sections
Use these standard sections to organize tests consistently:
// File: FieldValueTests.swift
import Testing
/// Tests for FieldValue type conversions and operations.
@Suite("Field Value")internalstructFieldValueTests{
// MARK: - Test Data Setup
privatestaticlettestString="Hello, World"privatestaticlettestNumber=42privatestaticlettestDate=Date()
// MARK: - String Value Tests
@Test("Create field value from string")internalfunc createFromString(){letvalue=FieldValue.string(Self.testString)
#expect(value.stringValue ==Self.testString)}@Test("Convert field value to string")internalfunc convertToString(){letvalue=FieldValue.string(Self.testString)letresult= value.stringValue
#expect(result ==Self.testString)}
// MARK: - Number Value Tests
@Test("Create field value from integer")internalfunc createFromInteger(){letvalue=FieldValue.int(Self.testNumber)
#expect(value.intValue ==Self.testNumber)}
// MARK: - Date Value Tests
@Test("Create field value from date")internalfunc createFromDate(){letvalue=FieldValue.date(Self.testDate)
#expect(value.dateValue ==Self.testDate)}}
When to use:
Small types with focused functionality
Utilities without complex behavior categories
Types with < 10 tests total
Self-contained functionality
Example 2: Extension Pattern (Two Level) - Parent With "Tests"
// File: UserManagerTests.swift
import Testing
/// Test suite for UserManager functionality.
@Suite("User Manager")internalenumUserManagerTests{}
// File: UserManagerTests+Basic.swift
import Testing
extensionUserManagerTests{
/// Basic UserManager functionality tests.
@Suite("Basic Tests")internalstructBasic{
// MARK: - Test Data Setup
privatestaticlettestUserID="user123"privatestaticlettestUsername="testuser"privatestaticlettestConfig=UserConfig(
autoSave:true,
cacheEnabled:true)
// MARK: - Initialization Tests
@Test("Initialize with valid configuration")internalfunc initializeWithValidConfiguration(){letmanager=UserManager(config:Self.testConfig)
#expect(manager.config.autoSave)
#expect(manager.config.cacheEnabled)}@Test("Initialize with default configuration")internalfunc initializeWithDefaultConfiguration(){letmanager=UserManager()
#expect(manager.config !=nil)}
// MARK: - User Fetching Tests
@Test("Fetch user successfully")internalfunc fetchUserSuccessfully()async{letmanager=UserManager(config:Self.testConfig)letuser=await manager.fetchUser(id:Self.testUserID)
#expect(user !=nil)}}}
// File: UserManagerTests+Validation.swift
import Testing
extensionUserManagerTests{
/// UserManager validation tests.
@Suite("Validation Tests")internalstructValidation{
// MARK: - Test Data Setup
privatestaticletvalidUserID="user123"privatestaticletemptyUserID=""privatestaticlettooLongUserID=String(repeating:"a", count:1000)
// MARK: - User ID Validation
@Test("Validate user ID with correct format")internalfunc validateCorrectFormat(){letisValid=UserManager.validateUserID(Self.validUserID)
#expect(isValid)}@Test("Reject empty user ID")internalfunc rejectEmptyUserID(){letisValid=UserManager.validateUserID(Self.emptyUserID)
#expect(!isValid)}@Test("Reject user ID that is too long")internalfunc rejectTooLongUserID(){letisValid=UserManager.validateUserID(Self.tooLongUserID)
#expect(!isValid)}}}
// File: UserManager+TestHelpers.swift
import Testing
extensionUserManager{
/// Test helper: Fetch user with simple boolean result.
internalfunc testFetchUser(id:String)async->Bool{do{letuser=tryawaitfetchUser(id: id)return user !=nil}catch{returnfalse}}
/// Create a test instance with default values.
internalstaticfunc testInstance()->UserManager{letconfig=UserConfig(autoSave:false, cacheEnabled:false)returnUserManager(config: config)}}
When to use:
Production types with multiple test categories
Tests that will grow over time
When different categories need different test data
// MARK: - Test Data Setup
// MARK: - Initialization Tests
// MARK: - Error Handling Tests
✅ Document suites and tests with comments
/// Tests for error handling scenarios.
@Suite("Error Handling")internalstructErrorHandling{...}
✅ Match file names to actual type names
// File: TypeNameTests+Category.swift
extensionTypeNameTests{structCategory{...} // ✅ Matches file name
}
Don't
❌ Use "Tests" in both parent enum and child struct
// DON'T DO THIS
internalenumTypeNameTests{}extensionTypeNameTests{structCategoryTests{...} // ❌ Redundant "Tests"
}
❌ Put multiple suite structs in one file
// DON'T DO THIS - Split into separate files
extensionTypeNameTests{structCategory1{...}structCategory2{...} // ❌ Should be in separate file
}
❌ Use classes for test suites
// DON'T DO THIS
@Suite("Tests")internalclassMyTests{...} // ❌ Use struct
// DO THIS
@Suite("Tests")internalstructMyTests{...} // ✅ Use struct
❌ Share mutable state between tests
// DON'T DO THIS
internalstructTests{privatevarcounter=0 // ❌ Mutable state
@Testfunc test1(){ counter +=1}@Testfunc test2(){ counter +=1} // ❌ Tests depend on order
}
// DON'T DO THIS
internalenumLevel1{}extensionLevel1{enumLevel2{}}extensionLevel1.Level2{enumLevel3{} // ❌ Too deeply nested
}
❌ Use XCTest patterns
// DON'T DO THIS
XCTAssertEqual(a, b) // ❌ XCTest API
XCTAssertTrue(condition) // ❌ XCTest API
// DO THIS
#expect(a == b) // ✅ Swift Testing API
#expect(condition) // ✅ Swift Testing API
❌ Mismatch file names and type names
// File: TypeNameTests+CategoryTests.swift ❌ Suggests both have "Tests"
extensionTypeNameTests{structCategory{...} // Actual child name doesn't match file name
}
// File: TypeNameTests+Category.swift ✅ Matches actual names
extensionTypeNameTests{structCategory{...}}
This demonstrates how the pattern scales from simple utilities (FieldValue) to complex managers (WebAuthTokenManager) in a real Swift package.
Conclusion
This hierarchical test organization pattern provides:
Clear structure: Logical grouping with enums and structs
Scalability: Easy to add new test categories
Consistency: Standardized naming and file organization
Discoverability: Predictable location for tests
Maintainability: Isolated test categories for focused changes
Key takeaways:
Never use "Tests" in both parent and child - Choose one location
One category per extension file - Keep files focused
Private static test data - Immutable data in suite structs
Use MARK sections - Organize tests within files
Test helpers in separate files - Extend production types
Choose a pattern for your project:
Pattern A: Parent has "Tests" (e.g., UserManagerTests+Validation)
Pattern B: Child has "Tests" (e.g., UserManager+ValidationTests)
Both patterns are equally valid. The important thing is to choose one and apply it consistently throughout your test suite. Document your choice in your project's contribution guidelines to ensure all contributors follow the same convention.
Swift Testing: Platform and OS Version Compatibility
A practical guide to conditionally enabling/disabling tests based on platform availability and OS versions.
Overview
Swift Testing provides .enabled(if:) and .disabled(if:) traits that allow you to conditionally run tests based on runtime conditions. This is especially useful for handling platform-specific features, OS version requirements, and API availability.
The Pattern: Platform Helper Utilities
Create a helper enum to centralize platform checks:
import Foundation
import Testing
/// Platform detection utilities for testing
internalenumPlatform{
/// Returns true if the current platform supports the required crypto functionality
/// Requires macOS 11.0+, iOS 14.0+, tvOS 14.0+, or watchOS 7.0+
internalstaticletisCryptoAvailable:Bool={if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0,*){returntrue}returnfalse}()}
This pattern:
Centralizes platform checks in one location
Uses #available to check OS version requirements
Provides compile-time constant that can be used with traits
Makes tests self-documenting about their requirements
Applying Traits to Test Suites
Use .enabled(if:) on entire test suites to skip all tests when requirements aren't met:
import Testing
@testableimport MistKit
@Suite("QueryFilter Tests",.enabled(if:Platform.isCryptoAvailable))structQueryFilterTests{
// All tests in this suite require crypto functionality
@Testfunc basicFilterCreation()asyncthrows{
// Test implementation
}@Testfunc complexFilterChaining()asyncthrows{
// Test implementation
}}
Benefits:
Single trait applies to all tests in the suite
Clear documentation at the suite level
Reduces redundant trait declarations on individual tests
Applying Traits to Individual Tests
For more granular control, apply traits to specific tests:
extensionServerToServerAuthManagerTests{@Suite("Server-to-Server Auth Manager Initialization",.enabled(if:Platform.isCryptoAvailable))internalstructInitializationTests{@Test("Initialization with private key callback",.enabled(if:Platform.isCryptoAvailable))internalfunc initializationWithPrivateKeyCallback()asyncthrows{guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0,*)else{Issue.record("ServerToServerAuthManager is not available on this operating system.")return}letmanager=tryServerToServerAuthManager(
keyID:"test-key-id",
privateKeyCallback:generateTestPrivateKey())
#expect(manager.keyID =="test-key-id")}}}
Defensive Pattern: Double-Checking Availability
Even with .enabled(if:) traits, it's good practice to add #available guards inside test bodies:
@Test("Private key signing validation",.enabled(if:Platform.isCryptoAvailable))internalfunc privateKeySigningValidation()asyncthrows{
// Defensive check - should never hit this, but provides clarity
guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0,*)else{Issue.record("Crypto functionality not available on this platform.")return}
// Test implementation using platform-specific APIs
letprivateKey=P256.Signing.PrivateKey()
// ...
}
Why this pattern?
Explicit documentation of OS requirements
Compiler enforcement of API availability
Clear error messages if trait logic fails
Makes code reviewers aware of platform constraints
Nested Suites with Traits
Traits can be applied at multiple levels:
@Suite("Authentication Middleware",.enabled(if:Platform.isCryptoAvailable))structAuthenticationMiddlewareTests{@Suite("Server-to-Server Tests",.enabled(if:Platform.isCryptoAvailable))structServerToServerTests{@Test("Middleware intercepts and adds authentication",.enabled(if:Platform.isCryptoAvailable))func middlewareInterceptsRequest()asyncthrows{
// Test implementation
}}@Suite("API Token Tests",.enabled(if:Platform.isCryptoAvailable))structAPITokenTests{
// Tests here
}}
Note: All conditions must pass for a test to run. If the outer suite is disabled, inner tests won't run regardless of their own traits.
Common Platform Checks
Here are examples of platform checks you might add to your Platform helper:
Use .disabled() when you need to temporarily skip tests:
@Test("Feature under development",.disabled("Waiting for API changes"))func incompleteFeature()asyncthrows{
// Test implementation
}
Or disable conditionally:
@Test("iOS-specific feature",.disabled(if: !Platform.isiOS,"This feature only works on iOS"))func iOSOnlyFeature()asyncthrows{
// Test implementation
}
Combining Multiple Conditions
You can apply multiple traits to a single test:
@Test("Advanced crypto feature",.enabled(if:Platform.isCryptoAvailable),.enabled(if:Platform.isMacOS),.timeLimit(.minutes(5)))func advancedCryptoFeature()asyncthrows{
// Test requires crypto AND macOS AND must complete in 5 minutes
}
Important: All conditions must be satisfied for the test to run. The first failing condition is reported as the skip reason.
Real-World Example from MistKit
Here's how MistKit uses this pattern across its test suite: