Loading...
Loading...
Swift Testing framework guide for writing tests with @Test, @Suite, #expect, #require, confirmation, parameterized tests, test tags, traits, withKnownIssue, XCTest UI testing, XCUITest, test plan, mocking, test doubles, testable architecture, snapshot testing, async test patterns, test organization, and test-driven development in Swift. Use when writing or migrating tests with Swift Testing framework, implementing parameterized tests, working with test traits, converting XCTest to Swift Testing, or setting up test organization and mocking patterns.
npx skill4agent add dpearson2699/swift-ios-skills swift-testingimport Testing
@Test("User can update their display name")
func updateDisplayName() {
var user = User(name: "Alice")
user.name = "Bob"
#expect(user.name == "Bob")
}@Test("Validates email format") // display name
@Test(.tags(.validation, .email)) // tags
@Test(.disabled("Server migration in progress")) // disabled
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil)) // conditional
@Test(.bug("https://github.com/org/repo/issues/42")) // bug reference
@Test(.timeLimit(.minutes(1))) // time limit
@Test("Timeout handling", .tags(.networking), .timeLimit(.seconds(30))) // combined// #expect records failure but continues execution
#expect(result == 42)
#expect(name.isEmpty == false)
#expect(items.count > 0, "Items should not be empty")
// #expect with error type checking
#expect(throws: ValidationError.self) {
try validate(email: "not-an-email")
}
// #expect with specific error value
#expect {
try validate(email: "")
} throws: { error in
guard let err = error as? ValidationError else { return false }
return err == .empty
}
// #require records failure AND stops test (like XCTUnwrap)
let user = try #require(await fetchUser(id: 1))
#expect(user.name == "Alice")
// #require for optionals -- unwraps or fails
let first = try #require(items.first)
#expect(first.isValid)#require#expect@Suite("Authentication Tests")
struct AuthTests {
let auth: AuthService
// init() runs before EACH test (like setUp in XCTest)
init() {
auth = AuthService(store: MockKeychain())
}
@Test func loginWithValidCredentials() async throws {
let result = try await auth.login(email: "test@test.com", password: "pass123")
#expect(result.isAuthenticated)
}
@Test func loginWithInvalidPassword() async throws {
#expect(throws: AuthError.invalidCredentials) {
try await auth.login(email: "test@test.com", password: "wrong")
}
}
}@Test("Email validation", arguments: [
("user@example.com", true),
("user@", false),
("@example.com", false),
("", false),
])
func validateEmail(email: String, isValid: Bool) {
#expect(EmailValidator.isValid(email) == isValid)
}
// From CaseIterable
@Test(arguments: Currency.allCases)
func currencyHasSymbol(currency: Currency) {
#expect(currency.symbol.isEmpty == false)
}
// Two collections: cartesian product of all combinations
@Test(arguments: [1, 2, 3], ["a", "b"])
func combinations(number: Int, letter: String) {
#expect(number > 0)
}
// Use zip for 1:1 pairing instead of cartesian product
@Test(arguments: zip(["USD", "EUR"], ["$", "€"]))
func currencySymbols(code: String, symbol: String) {
#expect(Currency(code: code).symbol == symbol)
}expectationfulfillwaitForExpectationsconfirmation@Test func notificationPosted() async {
await confirmation("Received notification") { confirm in
let observer = NotificationCenter.default.addObserver(
forName: .userLoggedIn, object: nil, queue: .main
) { _ in confirm() }
await authService.login()
NotificationCenter.default.removeObserver(observer)
}
}
// Exact count -- confirm must be called exactly 3 times
await confirmation("Items processed", expectedCount: 3) { confirm in
processor.onItemComplete = { _ in confirm() }
await processor.processAll()
}
// Range-based: at least once
await confirmation("Orders placed", expectedCount: 1...) { confirm in
truck.orderHandler = { _ in confirm() }
await truck.operate()
}
// Confirm something does NOT happen
await confirmation("No errors", expectedCount: 0) { confirm in
calculator.errorHandler = { _ in confirm() }
await calculator.compute()
}extension Tag {
@Tag static var networking: Self
@Tag static var database: Self
@Tag static var slow: Self
@Tag static var critical: Self
}
@Test(.tags(.networking, .slow))
func downloadLargeFile() async throws { ... }
// Tags on suites are inherited by all contained tests
@Suite(.tags(.database))
struct DatabaseTests {
@Test func insertUser() { ... } // inherits .database tag
}Tag@TagwithKnownIssue("Propane tank is empty") {
#expect(truck.grill.isHeating)
}
// Intermittent / flaky failures
withKnownIssue(isIntermittent: true) {
#expect(service.isReachable)
}
// Conditional known issue
withKnownIssue {
#expect(foodTruck.grill.isHeating)
} when: {
!hasPropane
}
// Match specific issues only
try withKnownIssue {
let level = try #require(foodTruck.batteryLevel)
#expect(level >= 0.8)
} matching: { issue in
guard case .expectationFailed(let expectation) = issue.kind else { return false }
return expectation.isRequired
}struct DatabaseFixture: TestScoping {
func provideScope(
for test: Test, testCase: Test.Case?,
performing body: @Sendable () async throws -> Void
) async throws {
let db = try await TestDatabase.create()
try await body()
try await db.destroy()
}
}.serializedprotocol UserRepository: Sendable {
func fetch(id: String) async throws -> User
func save(_ user: User) async throws
}
// Test double
struct MockUserRepository: UserRepository {
var users: [String: User] = [:]
var fetchError: Error?
func fetch(id: String) async throws -> User {
if let error = fetchError { throw error }
guard let user = users[id] else { throw NotFoundError() }
return user
}
func save(_ user: User) async throws { }
}@Observable
class ProfileViewModel {
private let repository: UserRepository
var user: User?
var error: Error?
init(repository: UserRepository) { self.repository = repository }
func load() async {
do { user = try await repository.fetch(id: currentUserID) }
catch { self.error = error }
}
}
@Test func loadUserSuccess() async {
let mock = MockUserRepository(users: ["1": User(name: "Alice")])
let vm = ProfileViewModel(repository: mock)
await vm.load()
#expect(vm.user?.name == "Alice")
}private struct UserRepositoryKey: EnvironmentKey {
static let defaultValue: any UserRepository = RemoteUserRepository()
}
extension EnvironmentValues {
var userRepository: any UserRepository {
get { self[UserRepositoryKey.self] }
set { self[UserRepositoryKey.self] = newValue }
}
}
// In previews and tests:
ContentView().environment(\.userRepository, MockUserRepository())@MainActor@Test func fetchUser() async throws {
let service = UserService(repository: MockUserRepository())
let user = try await service.fetch(id: "1")
#expect(user.name == "Alice")
}
@Test @MainActor func viewModelUpdatesOnMainActor() async {
let vm = ProfileViewModel(repository: MockUserRepository())
await vm.load()
#expect(vm.user != nil)
}protocol AppClock: Sendable { func sleep(for duration: Duration) async throws }
struct ImmediateClock: AppClock { func sleep(for duration: Duration) async throws { } }@Test func fetchUserNetworkError() async {
var mock = MockUserRepository()
mock.fetchError = URLError(.notConnectedToInternet)
let vm = ProfileViewModel(repository: mock)
await vm.load()
#expect(vm.user == nil)
#expect(vm.error is URLError)
}class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launchArguments = ["--ui-testing"]
app.launch()
}
func testLoginFlow() throws {
let emailField = app.textFields["Email"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap(); emailField.typeText("user@test.com")
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password123")
app.buttons["Sign In"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 10))
}
}struct LoginPage {
let app: XCUIApplication
var emailField: XCUIElement { app.textFields["Email"] }
var passwordField: XCUIElement { app.secureTextFields["Password"] }
var signInButton: XCUIElement { app.buttons["Sign In"] }
@discardableResult
func login(email: String, password: String) -> HomePage {
emailField.tap(); emailField.typeText(email)
passwordField.tap(); passwordField.typeText(password)
signInButton.tap()
return HomePage(app: app)
}
}func testFeedParsingPerformance() throws {
let data = try loadFixture("large-feed.json")
let metrics: [XCTMetric] = [XCTClockMetric(), XCTMemoryMetric()]
measure(metrics: metrics) {
_ = try? FeedParser.parse(data)
}
}import SnapshotTesting
import XCTest
class ProfileViewSnapshotTests: XCTestCase {
func testProfileView() {
let view = ProfileView(user: .preview)
assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))
// Dark mode
assertSnapshot(of: view.environment(\.colorScheme, .dark),
as: .image(layout: .device(config: .iPhone13)), named: "dark")
// Large Dynamic Type
assertSnapshot(of: view.environment(\.dynamicTypeSize, .accessibility3),
as: .image(layout: .device(config: .iPhone13)), named: "largeText")
}
}Tests/AppTests/ # Swift Testing (Models/, ViewModels/, Services/)
Tests/AppUITests/ # XCTest UI tests (Pages/, Flows/)
Tests/Fixtures/ # Test data (JSON, images)
Tests/Mocks/ # Shared mock implementations<TypeUnderTest>Tests.swiftfetchUserReturnsNilOnNetworkError()testFetchUser()Mock<ProtocolName>confirmationsleepinit()@SuitesleepconfirmationwithKnownIssueTask@MainActor@Test func generateReport() async throws {
let report = try generateReport()
// Attach the output for later inspection
Attachment(report.data, named: "report.json").record()
#expect(report.isValid)
}
// Attach from a file URL
@Test func processImage() async throws {
let output = try processImage()
try await Attachment(contentsOf: output.url, named: "result.png")
.record()
}AttachableAttachableAsImageexit()fatalError()preconditionFailure()@Test func invalidInputCausesExit() async {
await #expect(processExitsWith: .failure) {
processInvalidInput() // calls fatalError()
}
}@Test#expectfetchUserReturnsNilOnNetworkErrortestFetchUserconfirmation()Task.sleep.critical.slow