Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Core testing patterns for Swift using the Swift Testing framework, XCTest, and XCUITest.
@Test, #expect)sleep waits| Use Case | Framework | Notes |
|---|---|---|
| Unit + Integration | Swift Testing | Swift 6+, preferred. @Test, #expect, @Suite |
| Unit + Integration (legacy) | XCTest | Compatible with all Swift versions |
| UI Automation | XCUITest | Xcode Instruments + accessibility identifiers |
| Protocol-based mocking | See swift-protocol-di-testing | Companion skill for DI patterns |
import Testing
// Basic test
@Test func formatPrice() {
#expect(PriceFormatter.format(cents: 1000, currency: "USD") == "$10.00")
}
// Grouped suite
@Suite("PriceFormatter")
struct PriceFormatterTests {
@Test func formatsZero() {
#expect(PriceFormatter.format(cents: 0, currency: "USD") == "$0.00")
}
@Test func throwsOnNegative() throws {
#expect(throws: PriceError.negativeAmount) {
try PriceFormatter.format(cents: -1, currency: "USD")
}
}
}
// Parameterized tests
@Test("Formats various currencies", arguments: [
(100, "USD", "$1.00"),
(100, "EUR", "€1.00"),
(100, "GBP", "£1.00"),
])
func formatsMultipleCurrencies(cents: Int, currency: String, expected: String) {
#expect(PriceFormatter.format(cents: cents, currency: currency) == expected)
}
import Testing
@Test func fetchesUser() async throws {
let service = UserService(client: MockHTTPClient())
let user = try await service.fetchUser(id: "123")
#expect(user.id == "123")
#expect(user.name == "Test User")
}
// Timeout-bounded async tests
@Test(.timeLimit(.seconds(5)))
func completesWithinTimeout() async throws {
let result = try await slowOperation()
#expect(result != nil)
}
actor Counter {
private(set) var count = 0
func increment() { count += 1 }
}
@Test func actorIncrements() async {
let counter = Counter()
await counter.increment()
let count = await counter.count
#expect(count == 1)
}
import XCTest
class UserServiceTests: XCTestCase {
var sut: UserService!
var mockClient: MockHTTPClient!
override func setUp() {
super.setUp()
mockClient = MockHTTPClient()
sut = UserService(client: mockClient)
}
override func tearDown() {
sut = nil
mockClient = nil
super.tearDown()
}
func testFetchUser_returnsUser() async throws {
mockClient.stubbedResponse = .success(UserFixtures.testUser)
let user = try await sut.fetchUser(id: "123")
XCTAssertEqual(user.id, "123")
}
func testFetchUser_throwsOnNetworkError() async {
mockClient.stubbedResponse = .failure(URLError(.notConnectedToInternet))
do {
_ = try await sut.fetchUser(id: "123")
XCTFail("Expected error to be thrown")
} catch {
XCTAssertTrue(error is URLError)
}
}
}
import XCTest
import Combine
class ViewModelTests: XCTestCase {
var cancellables = Set<AnyCancellable>()
func testPublishesUpdatedValue() {
let vm = CounterViewModel()
var received: [Int] = []
let expectation = expectation(description: "receives values")
expectation.expectedFulfillmentCount = 3
vm.$count.sink { value in
received.append(value)
expectation.fulfill()
}.store(in: &cancellables)
vm.increment()
vm.increment()
waitForExpectations(timeout: 1)
XCTAssertEqual(received, [0, 1, 2])
}
}
import XCTest
class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launchArguments = ["--uitesting", "--reset-state"]
app.launch()
}
func testLoginFlow() throws {
// Use accessibilityIdentifier set in SwiftUI with .accessibilityIdentifier()
let emailField = app.textFields["login.email"]
let passwordField = app.secureTextFields["login.password"]
let loginButton = app.buttons["login.submit"]
emailField.tap()
emailField.typeText("user@example.com")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
XCTAssertTrue(app.staticTexts["dashboard.title"].waitForExistence(timeout: 3))
}
}
# Enable code coverage in scheme settings:
# Product → Scheme → Edit Scheme → Test → Code Coverage → ✓ Gather coverage
# Run with coverage from CLI
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-enableCodeCoverage YES | xcpretty
Target: 80%+ line coverage — enforce in CI via xccov or slather.
@Test func behaviour() { #expect(...) } — it fails to compile or returns wrong value#expect passswift test --enable-code-coverage, check reportWrong:
func testFetchUser_returnsUser() async throws {
let user = try await sut.fetchUser(id: "123")
XCTAssertEqual(user!.id, "123") // crashes on nil, hides the real failure
}
Correct:
func testFetchUser_returnsUser() async throws {
let user = try await sut.fetchUser(id: "123")
XCTAssertEqual(user.id, "123") // throws and reports properly on failure
}
Why: Force-unwrapping in tests causes a crash instead of a test failure, hiding the root cause and making CI output unreadable.
sleep to Wait for Async ResultsWrong:
@Test func publishesEvent() async {
sut.triggerAsyncWork()
try await Task.sleep(for: .seconds(1)) // arbitrary wait, brittle
#expect(sut.didPublish == true)
}
Correct:
@Test func publishesEvent() async throws {
let didPublish = try await withTimeout(.seconds(5)) {
await sut.triggerAsyncWork()
return sut.didPublish
}
#expect(didPublish == true)
}
Why: Fixed sleeps make tests slow and still flaky; use structured concurrency with bounded waits instead.
Wrong:
@Test func incrementUpdatesPrivateCount() {
let vm = CounterViewModel()
vm.increment()
#expect(vm._internalCount == 1) // accesses private state
}
Correct:
@Test func incrementUpdatesDisplayedValue() {
let vm = CounterViewModel()
vm.increment()
#expect(vm.displayText == "1") // tests observable output
}
Why: Tests coupled to private implementation break on safe refactors; test observable behavior instead.
Wrong:
var sharedService = UserService(client: MockHTTPClient())
@Test func firstTest() async throws {
sharedService.reset()
// ... test A modifies sharedService
}
@Test func secondTest() async throws {
// relies on sharedService state from firstTest — order-dependent
}
Correct:
@Suite("UserService")
struct UserServiceTests {
@Test func fetchUser() async throws {
let service = UserService(client: MockHTTPClient()) // fresh instance
// test A
}
@Test func handleError() async throws {
let service = UserService(client: MockHTTPClient()) // fresh instance
// test B
}
}
Why: Shared mutable state causes order-dependent flakiness; create fresh instances per test.
Wrong:
func testLoginFlow() throws {
app.buttons["Log In"].tap() // breaks if button label is localized or renamed
}
Correct:
func testLoginFlow() throws {
app.buttons["login.submit"].tap() // stable accessibilityIdentifier set in SwiftUI
}
Why: Querying by label text breaks with localization changes or copy rewrites; accessibilityIdentifier is stable and locale-independent.
@MainActor in test bodies: Marks the entire test synchronous — use await insteadcontinueAfterFailure = false in UI tests to stop on first failureaccessibilityIdentifier not label text for stable UI test element queries#expect over XCTAssert* in Swift Testing — better error messages and parameterization