swift-testing

Swift Testing Framework

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swift-testing" with this command: npx skills add bluewaves-creations/bluewaves-skills/bluewaves-creations-bluewaves-skills-swift-testing

Swift Testing Framework

Comprehensive guide to the modern Swift Testing framework, test organization, assertions, and Xcode Playgrounds for iOS 26 development.

Prerequisites

  • Swift 6.0+ (included in Xcode 16+)

  • Xcode 26+ recommended

Framework Overview

Swift Testing vs XCTest

Feature Swift Testing XCTest

Test marking @Test macro Method naming test*

Assertions #expect , #require

XCTAssert*

Test organization Structs, actors, classes XCTestCase subclass

Parallelism Parallel by default Process-based

Setup/Teardown init /deinit

setUp /tearDown

Import

import Testing

@Test Macro

Basic Test

import Testing

@Test func additionWorks() { let result = 2 + 2 #expect(result == 4) }

Test with Display Name

@Test("User can create account with valid email") func createAccountWithValidEmail() async throws { let account = try await AccountService.create(email: "test@example.com") #expect(account.email == "test@example.com") }

Async Tests

@Test func fetchUserReturnsData() async throws { let user = try await userService.fetch(id: "123") #expect(user.name == "John Doe") }

Throwing Tests

@Test func invalidEmailThrows() throws { #expect(throws: ValidationError.invalidEmail) { try validate(email: "not-an-email") } }

Assertions

#expect

Basic expectations:

@Test func basicExpectations() { let value = 42

// Equality
#expect(value == 42)

// Inequality
#expect(value != 0)

// Boolean
#expect(value > 0)

// With message
#expect(value == 42, "Value should be 42")

}

#expect with Expressions

@Test func expressionExpectations() { let array = [1, 2, 3]

#expect(array.count == 3)
#expect(array.contains(2))
#expect(!array.isEmpty)

let optional: String? = "hello"
#expect(optional != nil)

}

#require

Unwrap optionals and fail fast:

@Test func requireUnwrapping() throws { let optional: String? = "hello"

// Unwrap or fail test
let value = try #require(optional)

#expect(value == "hello")

}

@Test func requireCondition() throws { let array = [1, 2, 3]

// Fail if condition is false
try #require(array.count > 0)

let first = try #require(array.first)
#expect(first == 1)

}

Testing Throws

@Test func throwingBehavior() { // Expect any error #expect(throws: (any Error).self) { try riskyOperation() }

// Expect specific error type
#expect(throws: NetworkError.self) {
    try fetchData()
}

// Expect specific error value
#expect(throws: NetworkError.timeout) {
    try fetchWithTimeout()
}

}

@Test func noThrow() { // Expect no error #expect(throws: Never.self) { safeOperation() } }

Custom Failure Messages

@Test func customMessages() { let user = User(name: "Alice", age: 25)

#expect(user.age >= 18, "User must be an adult, but age was \(user.age)")

}

Test Organization

Test Suites with Structs

@Suite("User Authentication Tests") struct AuthenticationTests { @Test("Valid credentials succeed") func validLogin() async throws { let result = try await auth.login(user: "test", pass: "password") #expect(result.success) }

@Test("Invalid credentials fail")
func invalidLogin() async throws {
    let result = try await auth.login(user: "test", pass: "wrong")
    #expect(!result.success)
}

}

Nested Suites

@Suite("API Tests") struct APITests { @Suite("User Endpoints") struct UserEndpoints { @Test func getUser() async { } @Test func createUser() async { } }

@Suite("Post Endpoints")
struct PostEndpoints {
    @Test func getPosts() async { }
    @Test func createPost() async { }
}

}

Using Actors for Isolation

@Suite actor DatabaseTests { var database: TestDatabase

init() async throws {
    database = try await TestDatabase.create()
}

@Test
func insertWorks() async throws {
    try await database.insert(User(name: "Test"))
    let count = try await database.count(User.self)
    #expect(count == 1)
}

}

Setup and Teardown

@Suite struct DatabaseTests { let database: Database

init() async throws {
    // Setup - called before each test
    database = try await Database.createInMemory()
    try await database.migrate()
}

deinit {
    // Teardown - called after each test
    // Note: async cleanup should be done differently
}

@Test
func testInsert() async throws {
    try await database.insert(item)
    #expect(try await database.count() == 1)
}

}

Parameterized Tests

Basic Parameters

@Test("Validation", arguments: [ "test@example.com", "user@domain.org", "name@company.co.uk" ]) func validEmails(email: String) { #expect(isValidEmail(email)) }

Multiple Arguments

@Test("Addition", arguments: [ (2, 3, 5), (0, 0, 0), (-1, 1, 0), (100, 200, 300) ]) func addition(a: Int, b: Int, expected: Int) { #expect(a + b == expected) }

Zip Arguments

@Test(arguments: zip( ["hello", "world", "test"], [5, 5, 4] )) func stringLength(string: String, expectedLength: Int) { #expect(string.count == expectedLength) }

Custom Types as Arguments

struct TestCase: CustomTestStringConvertible { let input: String let expected: Int

var testDescription: String {
    "'\(input)' should have length \(expected)"
}

}

@Test("String lengths", arguments: [ TestCase(input: "hello", expected: 5), TestCase(input: "", expected: 0), TestCase(input: "Swift", expected: 5) ]) func stringLength(testCase: TestCase) { #expect(testCase.input.count == testCase.expected) }

Test Traits

.serialized

Run tests sequentially:

@Suite(.serialized) struct OrderDependentTests { @Test func step1() { } @Test func step2() { } @Test func step3() { } }

.disabled

Skip tests:

@Test(.disabled("Known bug, see issue #123")) func brokenFeature() { // Won't run }

@Test(.disabled(if: isCI, "Flaky on CI")) func flakyTest() { // Conditionally disabled }

.enabled

Conditionally enable:

@Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_SLOW_TESTS"] != nil)) func slowIntegrationTest() async throws { // Only runs when environment variable is set }

.tags

Organize with tags:

extension Tag { @Tag static var critical: Self @Tag static var slow: Self @Tag static var integration: Self }

@Test(.tags(.critical)) func criticalFeature() { }

@Test(.tags(.slow, .integration)) func slowIntegrationTest() async { }

.timeLimit

Set execution limit:

@Test(.timeLimit(.seconds(5))) func mustCompleteQuickly() async throws { // Fails if takes more than 5 seconds }

.bug

Reference known issues:

@Test(.bug("https://github.com/org/repo/issues/123", "Expected failure")) func knownIssue() { // Test expected to fail }

Parallel Execution

Default Parallel

Tests run in parallel by default:

@Suite struct ParallelTests { // These run concurrently @Test func test1() async { } @Test func test2() async { } @Test func test3() async { } }

Serial When Needed

@Suite(.serialized) struct SerialTests { static var sharedState = 0

@Test func first() {
    Self.sharedState = 1
    #expect(Self.sharedState == 1)
}

@Test func second() {
    Self.sharedState = 2
    #expect(Self.sharedState == 2)
}

}

Mocking and Test Doubles

Protocol-Based Mocking

protocol UserService { func fetch(id: String) async throws -> User }

struct MockUserService: UserService { var userToReturn: User? var errorToThrow: Error?

func fetch(id: String) async throws -> User {
    if let error = errorToThrow {
        throw error
    }
    guard let user = userToReturn else {
        throw TestError.notConfigured
    }
    return user
}

}

@Suite struct UserViewModelTests { @Test func fetchUserSuccess() async throws { var mockService = MockUserService() mockService.userToReturn = User(id: "1", name: "Test")

    let viewModel = UserViewModel(service: mockService)
    try await viewModel.loadUser(id: "1")

    #expect(viewModel.user?.name == "Test")
}

}

Spy for Verification

final class SpyUserService: UserService { var fetchCallCount = 0 var lastFetchedId: String?

func fetch(id: String) async throws -> User {
    fetchCallCount += 1
    lastFetchedId = id
    return User(id: id, name: "Test")
}

}

@Test func loadsUserOnAppear() async throws { let spy = SpyUserService() let viewModel = UserViewModel(service: spy)

await viewModel.loadUser(id: "123")

#expect(spy.fetchCallCount == 1)
#expect(spy.lastFetchedId == "123")

}

Testing SwiftUI

Testing Observable ViewModels

@Observable class CounterViewModel { var count = 0

func increment() {
    count += 1
}

}

@Suite struct CounterViewModelTests { @Test func incrementIncreasesCount() { let viewModel = CounterViewModel()

    viewModel.increment()

    #expect(viewModel.count == 1)
}

@Test
func multipleIncrements() {
    let viewModel = CounterViewModel()

    viewModel.increment()
    viewModel.increment()
    viewModel.increment()

    #expect(viewModel.count == 3)
}

}

Testing Async ViewModels

@Observable @MainActor class UserListViewModel { var users: [User] = [] var isLoading = false private let service: UserService

init(service: UserService) {
    self.service = service
}

func loadUsers() async {
    isLoading = true
    defer { isLoading = false }
    users = (try? await service.fetchAll()) ?? []
}

}

@Suite struct UserListViewModelTests { @Test @MainActor func loadUsersPopulatesArray() async { var mock = MockUserService() mock.usersToReturn = [User(id: "1", name: "Alice")]

    let viewModel = UserListViewModel(service: mock)
    await viewModel.loadUsers()

    #expect(viewModel.users.count == 1)
    #expect(viewModel.isLoading == false)
}

}

Migration from XCTest

Side-by-Side

Both frameworks can coexist:

// XCTest import XCTest

class LegacyTests: XCTestCase { func testOldStyle() { XCTAssertEqual(2 + 2, 4) } }

// Swift Testing import Testing

@Test func newStyle() { #expect(2 + 2 == 4) }

Mapping Assertions

XCTest Swift Testing

XCTAssertTrue(x)

#expect(x)

XCTAssertFalse(x)

#expect(!x)

XCTAssertEqual(a, b)

#expect(a == b)

XCTAssertNil(x)

#expect(x == nil)

XCTAssertNotNil(x)

try #require(x)

XCTAssertThrowsError

#expect(throws:)

XCTUnwrap(x)

try #require(x)

What to Keep in XCTest

  • Performance tests (measure {} )

  • UI tests (XCUITest)

  • Existing stable test suites

Xcode Playgrounds

#Playground Macro (iOS 26)

import SwiftUI

#Playground { let greeting = "Hello, Playgrounds!" print(greeting) }

#Playground("SwiftUI Preview") { struct ContentView: View { var body: some View { Text("Hello, World!") } }

ContentView()

}

Named Playground Blocks

#Playground("Data Processing") { let numbers = [1, 2, 3, 4, 5] let doubled = numbers.map { $0 * 2 } print(doubled) }

#Playground("API Simulation") { struct User: Codable { let name: String }

let json = #"{"name": "Alice"}"#
let user = try? JSONDecoder().decode(User.self, from: json.data(using: .utf8)!)
print(user?.name ?? "Unknown")

}

SwiftUI in Playgrounds

#Playground("Interactive UI") { struct Counter: View { @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

Counter()

}

Best Practices

  1. Descriptive Test Names

// GOOD @Test("User cannot login with expired token") func expiredTokenLogin() { }

// AVOID @Test func test1() { }

  1. One Assertion Focus

// GOOD: Focused test @Test func userNameIsCapitalized() { let user = User(name: "alice") #expect(user.displayName == "Alice") }

// AVOID: Multiple unrelated assertions @Test func userTests() { let user = User(name: "alice") #expect(user.displayName == "Alice") #expect(user.email != nil) #expect(user.createdAt <= Date()) }

  1. Use Structs for Test Suites

// GOOD: Struct-based, each test gets fresh instance @Suite struct UserTests { let service = UserService()

@Test func fetch() { }
@Test func create() { }

}

  1. Parameterize Repetitive Tests

// GOOD: Parameterized @Test(arguments: ["", " ", " "]) func emptyStringsAreInvalid(input: String) { #expect(!isValid(input)) }

// AVOID: Duplicated tests @Test func emptyIsInvalid() { #expect(!isValid("")) } @Test func spaceIsInvalid() { #expect(!isValid(" ")) } @Test func spacesAreInvalid() { #expect(!isValid(" ")) }

  1. Use Tags for Organization

extension Tag { @Tag static var unit: Self @Tag static var integration: Self @Tag static var slow: Self }

// Filter in Xcode or command line // swift test --filter "unit"

Official Resources

  • Swift Testing Documentation

  • Testing with Xcode

  • WWDC24: Meet Swift Testing

  • WWDC23: Prototype with Xcode Playgrounds

  • Migrating a test from XCTest

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

photographer-testino

No summary provided by upstream source.

Repository SourceNeeds Review
General

photographer-lindbergh

No summary provided by upstream source.

Repository SourceNeeds Review
General

photographer-lachapelle

No summary provided by upstream source.

Repository SourceNeeds Review
General

photographer-vonunwerth

No summary provided by upstream source.

Repository SourceNeeds Review