Testing is how you verify your code works correctly and keeps working as you make changes. Swift provides XCTest for unit and UI testing, and Swift 5.9+ introduced the new Swift Testing framework with improved syntax and features.

XCTest Basics

The traditional testing framework in Swift:

import XCTest

class CalculatorTests: XCTestCase {
    var calculator: Calculator!

    override func setUp() {
        super.setUp()
        calculator = Calculator()
    }

    override func tearDown() {
        calculator = nil
        super.tearDown()
    }

    func testAddition() {
        let result = calculator.add(2, 3)
        XCTAssertEqual(result, 5)
    }

    func testDivision() {
        let result = calculator.divide(10, 2)
        XCTAssertEqual(result, 5)
    }

    func testDivisionByZero() {
        XCTAssertThrowsError(try calculator.divide(10, 0))
    }
}

New Swift Testing Framework

Modern testing with better syntax (Swift 5.9+):

import Testing

@Suite
struct CalculatorTests {
    let calculator = Calculator()

    @Test
    func addition() {
        #expect(calculator.add(2, 3) == 5)
    }

    @Test
    func subtraction() {
        #expect(calculator.subtract(10, 3) == 7)
    }

    @Test
    func divisionByZero() {
        #expect(throws: DivisionError.self) {
            try calculator.divide(10, 0)
        }
    }
}

Parameterized Tests

Test multiple inputs easily:

@Suite
struct ValidationTests {
    @Test(arguments: [
        ("user@example.com", true),
        ("invalid-email", false),
        ("@example.com", false),
        ("user@.com", false)
    ])
    func emailValidation(email: String, expectedValid: Bool) {
        let isValid = EmailValidator.validate(email)
        #expect(isValid == expectedValid)
    }
}

Async Testing

Test asynchronous code:

@Suite
struct NetworkTests {
    @Test
    func fetchUser() async throws {
        let user = try await NetworkManager.fetchUser(id: "123")
        #expect(user.id == "123")
        #expect(!user.name.isEmpty)
    }

    @Test
    func timeout() async throws {
        try await #require(timeout: .seconds(5)) {
            try await slowOperation()
        }
    }
}

Test Suites and Organization

Organize tests into suites:

@Suite("Authentication")
struct AuthTests {
    @Test("Login with valid credentials")
    func validLogin() async throws {
        let result = try await AuthService.login(
            email: "test@example.com",
            password: "password123"
        )
        #expect(result.isSuccess)
    }

    @Test("Login with invalid credentials")
    func invalidLogin() async throws {
        #expect(throws: AuthError.invalidCredentials) {
            try await AuthService.login(
                email: "test@example.com",
                password: "wrong"
            )
        }
    }
}

Testing SwiftUI Views

Test SwiftUI views and view models:

import Testing
import SwiftUI

@Suite
struct CounterViewTests {
    @Test
    func counterIncrement() {
        let viewModel = CounterViewModel()

        #expect(viewModel.count == 0)
        viewModel.increment()
        #expect(viewModel.count == 1)
    }

    @Test
    func counterReset() {
        let viewModel = CounterViewModel()
        viewModel.increment()
        viewModel.increment()
        viewModel.reset()

        #expect(viewModel.count == 0)
    }
}

Mocking and Test Doubles

Create test doubles for dependencies:

protocol DataService {
    func fetchData() async throws -> [String]
}

struct MockDataService: DataService {
    var mockData: [String] = []
    var shouldThrow = false

    func fetchData() async throws -> [String] {
        if shouldThrow {
            throw NetworkError.serverError
        }
        return mockData
    }
}

@Suite
struct ViewModelTests {
    @Test
    func loadDataSuccess() async throws {
        let mockService = MockDataService(mockData: ["Item 1", "Item 2"])
        let viewModel = DataViewModel(service: mockService)

        try await viewModel.loadData()
        #expect(viewModel.items.count == 2)
    }
}

Performance Testing

Measure performance:

@Test
func performanceTest() {
    measure {
        // Code to measure
        let result = complexCalculation()
    }
}

Code Coverage

Enable code coverage in Xcode:

  1. Edit Scheme → Test

  2. Options → Code Coverage

  3. Check "Gather coverage for all targets"

Best Practices

  1. Test one thing - Each test should verify one behavior

  2. Use descriptive names - Make test names clear

  3. Arrange-Act-Assert - Structure tests clearly

  4. Keep tests fast - Fast tests encourage frequent running

  5. Mock external dependencies - Isolate units under test

  6. Test edge cases - Don't just test happy paths

  7. Keep tests independent - Tests shouldn't depend on each other

Wrap up

Swift provides powerful testing tools through both XCTest and the new Swift Testing framework. Writing tests improves code quality, catches bugs early, and makes refactoring safer.

Resources:

Keep reading

Read more

3 min read Focus State in SwiftUI Read article 3 min read Building SwiftUI Debugging Utilities Read article 3 min read Observable Geocoder Read article

Share

Share this article

Share Bluesky Mastodon Twitter LinkedIn Facebook