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))
}
}#expectrecords a failure and continues;#requirestops 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")
}
}| Tool | Answers | Cost |
|---|---|---|
| Preview | How does this state look? | Seconds, no simulator boot |
| Unit test | Is the logic correct? | Milliseconds |
| UI test | Does the flow work end to end? | Minutes, brittle to copy changes |
| Instruments | Where 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.Related
Architecture: MVVM, observation and dependency injection Release engineering: TestFlight, CI and App Store Connect
Last refreshed 2026-09-18.