DEV Community

ArshTechPro
ArshTechPro

Posted on

Xcode 26 - Swift Testing Attachments

Swift Testing Attachments in Xcode 26 transform test debugging from guesswork into precise diagnosis. Let's explore how this game-changing feature works.

The Old Way: Print Statement Hell

Picture this familiar scenario:

@Test
func userProfileTest() async throws {
    let user = try await apiClient.fetchUser(id: "12345")
    #expect(user.name == "John Doe")
    #expect(user.isActive == true)
}
Enter fullscreen mode Exit fullscreen mode

When this fails, you get:

❌ Expected true, got false at line 4
Enter fullscreen mode Exit fullscreen mode

What you actually need to know:

  • What did the API return?
  • Was the response malformed?
  • What was the network status?
  • What went wrong?

Before attachments, debugging meant adding print statements, re-running tests, digging through console output, and repeating this cycle endlessly.

The New Way: Rich Context Automatically

Swift Testing Attachments let you capture any data during test execution and attach it to test results. When tests fail, you get complete context about what actually happened.

Basic Concept

Think of attachments as "evidence" that gets automatically collected and stored with your test results. When something goes wrong, you can immediately see exactly what happened.

import Testing

@Test
func userProfileWithContext() async throws {
    let user = try await apiClient.fetchUser(id: "12345")

    // Attach the API response for debugging
    Attachment.record(user, named: "User API Response")

    #expect(user.name == "John Doe")
    #expect(user.isActive == true)
}
Enter fullscreen mode Exit fullscreen mode

Now when the test fails, you can see exactly what the API returned!

Real-World Examples

🌐 Network Request Debugging

The most common use case - understanding what your API actually returned:

@Test
func orderCreation() async throws {
    let orderRequest = OrderRequest(
        items: [Item(id: "abc", quantity: 2)],
        shippingAddress: testAddress
    )

    // Attach request and response for debugging
    Attachment.record(orderRequest, named: "Order Request")

    let response = try await orderService.createOrder(orderRequest)
    Attachment.record(response, named: "Order Response")

    // If there's an error, attach details
    if let error = response.error {
        let errorDetails = """
        Status: \(error.statusCode)
        Message: \(error.message)
        Time: \(Date())
        """
        Attachment.record(errorDetails, named: "Error Details")
    }

    #expect(response.orderId != nil)
}
Enter fullscreen mode Exit fullscreen mode

πŸ“± UI Testing with Screenshots

Perfect for visual testing where you need to see what actually happened:

@Test
func loginFlow() throws {
    let app = XCUIApplication()
    app.launch()

    // Capture initial state
    let screenshot = app.screenshot()
    Attachment.record(screenshot.pngRepresentation, named: "Initial State")

    app.buttons["Login"].tap()

    // Capture after login attempt
    let loginScreenshot = app.screenshot()
    Attachment.record(loginScreenshot.pngRepresentation, named: "After Login")

    #expect(app.staticTexts["Welcome"].exists)
}
Enter fullscreen mode Exit fullscreen mode

πŸ—„οΈ Database State Debugging

Understand exactly what's in your database when tests fail:

@Test
func userDeletion() async throws {
    let user = try await createTestUser(email: "test@example.com")

    // Capture before and after states
    let beforeUsers = try await database.fetchAllUsers()
    Attachment.record(beforeUsers, named: "Users Before Deletion")

    try await userService.deleteUser(user.id)

    let afterUsers = try await database.fetchAllUsers()
    Attachment.record(afterUsers, named: "Users After Deletion")

    #expect(!afterUsers.contains { $0.id == user.id })
}
Enter fullscreen mode Exit fullscreen mode

Smart Attachment Strategies

Only Attach What You Need

Attachments can consume storage, so be strategic:

@Test
func apiResponseTest() async throws {
    let response = try await apiClient.fetchLargeDataSet()

    // ❌ Don't attach massive data unnecessarily
    // Attachment.record(response.fullDataSet, named: "Complete Dataset") // Could be 100MB!

    // βœ… Attach summary instead
    let summary = DataSummary(
        count: response.fullDataSet.count,
        firstItem: response.fullDataSet.first,
        lastItem: response.fullDataSet.last
    )
    Attachment.record(summary, named: "Dataset Summary")

    // βœ… Only attach full data if something's wrong
    if response.fullDataSet.isEmpty {
        Attachment.record(response.fullDataSet, named: "Empty Dataset Debug")
    }
}
Enter fullscreen mode Exit fullscreen mode

Conditional Attachments

Only collect expensive debug data when needed:

@Test
func complexOperation() async throws {
    let result = try await performComplexOperation()

    // Only attach expensive debug data if confidence is low
    if result.confidence < 0.8 {
        let debugData = try await gatherDetailedDiagnostics()
        Attachment.record(debugData, named: "Low Confidence Debug")
    }

    #expect(result.success)
}
Enter fullscreen mode Exit fullscreen mode

What Can You Attach?

Swift Testing supports attaching:

  • Strings - Error messages, logs, debug output
  • Data - Raw bytes, JSON responses, file contents
  • Any Codable object - Your custom structs, API responses
  • Files - Screenshots, generated reports, log files
// String attachment
Attachment.record("Debug info: \(debugMessage)", named: "Debug Log")

// Custom object attachment
struct TestEnvironment: Codable {
    let device: String
    let osVersion: String
    let timestamp: Date
}

let environment = TestEnvironment(
    device: UIDevice.current.model,
    osVersion: UIDevice.current.systemVersion,
    timestamp: Date()
)
Attachment.record(environment, named: "Test Environment")
Enter fullscreen mode Exit fullscreen mode

Rich Context

// NEW: Rich debugging context
@Test
func dataProcessing() throws {
    let input = generateTestData()
    Attachment.record(input, named: "Input Data")

    let result = processor.process(input)
    Attachment.record(result, named: "Processing Result")

    // Attach processing stats
    let stats = ProcessingStats(
        inputSize: input.count,
        outputSize: result.count,
        processingTime: processor.lastProcessingTime
    )
    Attachment.record(stats, named: "Performance Stats")

    #expect(result.isValid)
}
Enter fullscreen mode Exit fullscreen mode

Getting Started

Requirements:

  • Xcode 26 (includes Swift 6.2)
  • macOS 15.6 or later (for Xcode 26)

Setup:

  1. Install Xcode 26 from the Mac App Store or Apple Developer portal
  2. Import the Testing framework: import Testing
  3. Use @Test instead of XCTest's func testSomething()
  4. Replace XCTAssert with #expect
  5. Add attachments with Attachment.record()

The Bottom Line

  • Rich context automatically captured
  • Visual debugging with actual data
  • Complete understanding of failures
  • One-click access to all relevant information

Your test failures become informative. Every attachment tells a story about what actually happened during test execution.


Swift Testing Attachments are available in Swift 6.2, which requires Xcode 26. Xcode 26 includes Swift 6.2 and is required to access these new testing features. They work on all platforms that support Swift Testing.

Top comments (1)

Collapse
 
arshtechpro profile image
ArshTechPro

Attachments are included in test reports in Xcode or written to disk when tests are run at the command line. To create an attachment, you need a value of some type that conforms to Attachable.