Testing, debugging and Instruments

Write fast unit tests with Swift Testing, drive the interface with XCUITest, use previews as a development tool, and profile leaks and slow frames in Instruments.

Unit tests with Swift Testing

import Testing

struct PriceFormatterTests {
    @Test("formats zero as a currency string")
    func formatsZero() {
        #expect(PriceFormatter().string(from: 0, currency: "GBP") == "0.00 GBP")
    }

    @Test("rejects negative stock", arguments: [-1, -100])
    func rejectsNegative(quantity: Int) {
        #expect(throws: CartError.self) {
            try CartItem(sku: "A1", quantity: quantity)
        }
    }

    @Test
    func discountAppliesBeforeTax() async throws {
        let cart = try await Cart(items: [.fixture(unitPrice: 100, quantity: 3)])
        let total = try cart.total(discount: 0.1, tax: 0.2)
        #expect(total == Decimal(324))
    }
}
  • #expect records a failure and continues; #require stops the test when a precondition fails.
  • Pass arguments: to run the same test over a table of inputs — parameterised tests replace copy-pasted methods.
  • Tests run in parallel by default, so shared mutable state must be eliminated or protected.
  • Keep the unit target free of UIKit and SwiftUI imports; those tests should run in milliseconds with no simulator.

UI tests and previews

import XCTest

final class CheckoutUITests: XCTestCase {
    func testAddingAnItemShowsItInTheCart() {
        let app = XCUIApplication()
        app.launchArguments = ["-uiTestSeed", "empty-cart"]
        app.launch()

        app.buttons["Add to cart"].firstMatch.tap()

        let badge = app.staticTexts["cart-count"]
        XCTAssertTrue(badge.waitForExistence(timeout: 5))
        XCTAssertEqual(badge.label, "1")
    }
}
ToolAnswersCost
PreviewHow does this state look?Seconds, no simulator boot
Unit testIs the logic correct?Milliseconds
UI testDoes the flow work end to end?Minutes, brittle to copy changes
InstrumentsWhere do time and memory go?Slow, run before a release

Finding leaks and slow frames

# run the app and attach Instruments from the command line
xcrun xctrace record --template "Leaks" \
  --device "iPhone 16" \
  --launch com.example.app \
  --output leaks.trace

# time profiler for a scroll-heavy screen
xcrun xctrace record --template "Time Profiler" \
  --launch com.example.app --output time.trace \
  --time-limit 60s
💡
Instrument the release build with dSYMs, not a debug build. Debug builds disable optimisations, so profile numbers from them are misleading and a leak found there may not exist in production.

FAQ

How much test coverage is enough?
Enough that you would ship a refactor without manually re-testing the whole app. Coverage percentage is a poor target; cover the logic with branches, the parsing of untrusted input, and the critical user flows.
Why is my UI test flaky?
Almost always because it depends on timing or on a shared backend. Seed deterministic state through launch arguments, wait for elements with waitForExistence, and never assert on an element that an animation may still be moving.

Architecture: MVVM, observation and dependency injection Release engineering: TestFlight, CI and App Store Connect

Last refreshed 2026-09-18.