swift-testing

Test Swift applications - XCTest, Swift Testing, UI tests, mocking, TDD, CI/CD

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 pluginagentmarketplace/custom-plugin-swift/pluginagentmarketplace-custom-plugin-swift-swift-testing

Swift Testing Skill

Comprehensive testing strategies for Swift applications using XCTest and Swift Testing framework.

Prerequisites

  • Xcode 15+ installed
  • Understanding of dependency injection
  • Familiarity with async/await

Parameters

parameters:
  framework:
    type: string
    enum: [xctest, swift_testing]
    default: swift_testing
  test_type:
    type: string
    enum: [unit, integration, ui, snapshot]
    default: unit
  coverage_target:
    type: number
    default: 80
    description: Target code coverage percentage
  ci_platform:
    type: string
    enum: [xcode_cloud, github_actions, gitlab_ci, none]
    default: github_actions

Topics Covered

Test Frameworks

FrameworkMin VersionKey Features
XCTestiOS 2.0+XCTestCase, expectations
Swift TestingiOS 17+ / Swift 5.9+@Test, #expect, traits

Test Types

TypeScopeSpeed
UnitSingle function/classFastest
IntegrationMultiple componentsMedium
UIFull user flowsSlowest
SnapshotVisual regressionMedium

Testing Patterns

PatternPurpose
AAAArrange, Act, Assert
Given-When-ThenBDD style
Test DoublesMock, Stub, Spy, Fake

Code Examples

Swift Testing (iOS 17+ / Swift 5.9+)

import Testing
@testable import MyApp

@Suite("ShoppingCart Tests")
struct ShoppingCartTests {
    var cart: ShoppingCart
    var mockRepository: MockProductRepository

    init() {
        mockRepository = MockProductRepository()
        cart = ShoppingCart(repository: mockRepository)
    }

    @Test("adding product increases count")
    func addProduct() async throws {
        let product = Product(id: "1", name: "Widget", price: 9.99)

        cart.add(product)

        #expect(cart.items.count == 1)
        #expect(cart.items.first?.product == product)
    }

    @Test("adding same product increases quantity")
    func addSameProductTwice() {
        let product = Product(id: "1", name: "Widget", price: 9.99)

        cart.add(product)
        cart.add(product)

        #expect(cart.items.count == 1)
        #expect(cart.items.first?.quantity == 2)
    }

    @Test("total calculates correctly")
    func calculateTotal() {
        cart.add(Product(id: "1", name: "A", price: 10.00))
        cart.add(Product(id: "2", name: "B", price: 20.00))

        #expect(cart.total == 30.00)
    }

    @Test("checkout requires non-empty cart", .tags(.checkout))
    func checkoutEmptyCart() async {
        await #expect(throws: CartError.empty) {
            try await cart.checkout()
        }
    }

    @Test("checkout with valid cart", .tags(.checkout))
    func checkoutSuccess() async throws {
        cart.add(Product(id: "1", name: "Widget", price: 9.99))
        mockRepository.checkoutResult = .success(Order(id: "order-1"))

        let order = try await cart.checkout()

        #expect(order.id == "order-1")
        #expect(cart.items.isEmpty)
    }

    @Test(arguments: [0, 1, 5, 10])
    func discountTiers(quantity: Int) {
        let discount = cart.calculateDiscount(forQuantity: quantity)

        switch quantity {
        case 0..<5: #expect(discount == 0)
        case 5..<10: #expect(discount == 0.05)
        default: #expect(discount == 0.10)
        }
    }
}

XCTest with Async

import XCTest
@testable import MyApp

final class ProductServiceTests: XCTestCase {
    var sut: ProductService!
    var mockAPI: MockAPIClient!

    override func setUp() {
        super.setUp()
        mockAPI = MockAPIClient()
        sut = ProductService(api: mockAPI)
    }

    override func tearDown() {
        sut = nil
        mockAPI = nil
        super.tearDown()
    }

    func test_fetchProducts_success() async throws {
        // Arrange
        let expectedProducts = [Product(id: "1", name: "Test", price: 9.99)]
        mockAPI.productsResult = .success(expectedProducts)

        // Act
        let products = try await sut.fetchProducts()

        // Assert
        XCTAssertEqual(products, expectedProducts)
        XCTAssertTrue(mockAPI.fetchProductsCalled)
    }

    func test_fetchProducts_networkError_throws() async {
        // Arrange
        mockAPI.productsResult = .failure(NetworkError.noConnection)

        // Act & Assert
        do {
            _ = try await sut.fetchProducts()
            XCTFail("Expected error to be thrown")
        } catch {
            XCTAssertTrue(error is NetworkError)
        }
    }

    func test_fetchProducts_retries_onTransientError() async throws {
        // Arrange
        var attempts = 0
        mockAPI.onFetchProducts = {
            attempts += 1
            if attempts < 3 {
                throw NetworkError.timeout
            }
            return [Product(id: "1", name: "Test", price: 9.99)]
        }

        // Act
        _ = try await sut.fetchProductsWithRetry(maxAttempts: 3)

        // Assert
        XCTAssertEqual(attempts, 3)
    }
}

Mock Implementation

// Protocol for abstraction
protocol APIClientProtocol {
    func fetchProducts() async throws -> [Product]
    func createOrder(_ order: CreateOrderRequest) async throws -> Order
}

// Production implementation
final class APIClient: APIClientProtocol {
    func fetchProducts() async throws -> [Product] {
        // Real implementation
    }

    func createOrder(_ order: CreateOrderRequest) async throws -> Order {
        // Real implementation
    }
}

// Test mock
final class MockAPIClient: APIClientProtocol {
    var productsResult: Result<[Product], Error> = .success([])
    var orderResult: Result<Order, Error> = .success(Order(id: "mock"))

    var fetchProductsCalled = false
    var fetchProductsCallCount = 0
    var createOrderCalled = false
    var lastOrderRequest: CreateOrderRequest?

    var onFetchProducts: (() async throws -> [Product])?

    func fetchProducts() async throws -> [Product] {
        fetchProductsCalled = true
        fetchProductsCallCount += 1

        if let handler = onFetchProducts {
            return try await handler()
        }

        return try productsResult.get()
    }

    func createOrder(_ order: CreateOrderRequest) async throws -> Order {
        createOrderCalled = true
        lastOrderRequest = order
        return try orderResult.get()
    }

    func reset() {
        productsResult = .success([])
        orderResult = .success(Order(id: "mock"))
        fetchProductsCalled = false
        fetchProductsCallCount = 0
        createOrderCalled = false
        lastOrderRequest = nil
        onFetchProducts = nil
    }
}

UI Testing with Page Object Pattern

import XCTest

// Page Object
struct LoginPage {
    let app: XCUIApplication

    var usernameField: XCUIElement {
        app.textFields["username"]
    }

    var passwordField: XCUIElement {
        app.secureTextFields["password"]
    }

    var loginButton: XCUIElement {
        app.buttons["login"]
    }

    var errorMessage: XCUIElement {
        app.staticTexts["errorMessage"]
    }

    func login(username: String, password: String) {
        usernameField.tap()
        usernameField.typeText(username)

        passwordField.tap()
        passwordField.typeText(password)

        loginButton.tap()
    }

    func waitForLogin(timeout: TimeInterval = 5) -> Bool {
        !usernameField.waitForExistence(timeout: timeout)
    }
}

// UI Test
final class LoginUITests: XCTestCase {
    var app: XCUIApplication!
    var loginPage: LoginPage!

    override func setUp() {
        super.setUp()
        continueAfterFailure = false

        app = XCUIApplication()
        app.launchArguments = ["--uitesting", "--reset-state"]
        app.launch()

        loginPage = LoginPage(app: app)
    }

    func test_login_withValidCredentials_navigatesToHome() {
        loginPage.login(username: "testuser", password: "password123")

        XCTAssertTrue(loginPage.waitForLogin())
        XCTAssertTrue(app.tabBars["mainTabBar"].exists)
    }

    func test_login_withInvalidCredentials_showsError() {
        loginPage.login(username: "wrong", password: "wrong")

        XCTAssertTrue(loginPage.errorMessage.waitForExistence(timeout: 5))
        XCTAssertEqual(loginPage.errorMessage.label, "Invalid credentials")
    }
}

GitHub Actions CI

name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: sudo xcode-select -s /Applications/Xcode_15.2.app

      - name: Build and Test
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' \
            -resultBundlePath TestResults.xcresult \
            -enableCodeCoverage YES \
            CODE_SIGNING_ALLOWED=NO

      - name: Upload Results
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: test-results
          path: TestResults.xcresult

      - name: Coverage Report
        run: |
          xcrun xccov view --report TestResults.xcresult

Troubleshooting

Common Issues

IssueCauseSolution
Flaky testsShared stateAdd setUp/tearDown cleanup
Async timeoutMissing fulfillmentCall fulfill() or increase timeout
UI element not foundWrong identifierCheck accessibilityIdentifier
Mock not workingWrong initializationVerify dependency injection
Coverage lowUntested pathsAdd edge case tests

Debug Tips

// Print XCUIElement hierarchy
print(app.debugDescription)

// Wait for condition
let exists = element.waitForExistence(timeout: 5)

// Take screenshot on failure
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways
add(attachment)

Validation Rules

validation:
  - rule: test_naming
    severity: info
    check: Use descriptive test names (test_method_condition_result)
  - rule: one_assertion
    severity: info
    check: Prefer one logical assertion per test
  - rule: no_test_interdependence
    severity: error
    check: Tests must not depend on each other

Usage

Skill("swift-testing")

Related Skills

  • swift-fundamentals - Code to test
  • swift-concurrency - Testing async code
  • swift-architecture - Testable architecture

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.

Automation

swift-uikit

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-macos

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-architecture

No summary provided by upstream source.

Repository SourceNeeds Review