Skip to content
All essays
iOSMarch 27, 202513 min

Swift Testing: XCTest and Swift Testing Framework

Unit testing, UI testing, and TDD practices in Swift applications.

Ü
Ümit Uz
Mobile & Full Stack Developer

Writing quality code in Swift applications requires testing. Write comprehensive tests with XCTest framework and the new Swift Testing Framework.

Unit Testing with XCTest

Simple Unit Test

swift
import XCTest
@testable import MyApp

class MathUtilsTests: XCTestCase {
    func testAddition() {
        // Given
        let a = 5
        let b = 3

        // When
        let result = MathUtils.add(a, b)

        // Then
        XCTAssertEqual(result, 8)
    }

    func testDivision_byZero_throwsError() {
        XCTAssertThrowsError(try MathUtils.divide(10, by: 0))
    }
}

Setup and Teardown

swift
class UserViewModelTests: XCTestCase {
    var viewModel: UserViewModel!
    var mockService: MockUserService!

    override func setUp() {
        super.setUp()
        mockService = MockUserService()
        viewModel = UserViewModel(service: mockService)
    }

    override func tearDown() {
        viewModel = nil
        mockService = nil
        super.tearDown()
    }

    func testLoadUsers_success() async throws {
        // Given
        let expectedUsers = [
            User(id: "1", name: "John"),
            User(id: "2", name: "Jane")
        ]
        mockService.usersToReturn = expectedUsers

        // When
        try await viewModel.loadUsers()

        // Then
        XCTAssertEqual(viewModel.users.count, 2)
        XCTAssertEqual(viewModel.users.first?.name, "John")
    }
}

Mocking

Manual Mock

swift
protocol UserServiceProtocol {
    func fetchUsers() async throws -> [User]
}

class MockUserService: UserServiceProtocol {
    var usersToReturn: [User] = []
    var shouldThrowError = false

    func fetchUsers() async throws -> [User] {
        if shouldThrowError {
            throw NetworkError.requestFailed
        }
        return usersToReturn
    }
}

Mocking with Dependency Injection

swift
class UserViewModel: ObservableObject {
    @Published var users: [User] = []
    private let service: UserServiceProtocol

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

    func loadUsers() async throws {
        users = try await service.fetchUsers()
    }
}

// In test
class UserViewModelTests: XCTestCase {
    func testLoadUsers() async throws {
        let mockService = MockUserService()
        let viewModel = UserViewModel(service: mockService)

        mockService.usersToReturn = [
            User(id: "1", name: "John")
        ]

        try await viewModel.loadUsers()

        XCTAssertEqual(viewModel.users.count, 1)
    }
}

UI Testing

Simple UI Test

swift
class LoginUITests: XCTestCase {
    var app: XCUIApplication!

    override func setUp() {
        super.setUp()
        app = XCUIApplication()
        app.launchArguments = ["--uitesting"]
        app.launch()
    }

    func testSuccessfulLogin() {
        // Find and fill email field
        let emailField = app.textFields["emailField"]
        XCTAssertTrue(emailField.exists)
        emailField.tap()
        emailField.typeText("test@example.com")

        // Find and fill password field
        let passwordField = app.secureTextFields["passwordField"]
        XCTAssertTrue(passwordField.exists)
        passwordField.tap()
        passwordField.typeText("password123")

        // Tap login button
        let loginButton = app.buttons["loginButton"]
        XCTAssertTrue(loginButton.exists)
        loginButton.tap()

        // Verify successful login
        let welcomeLabel = app.staticTexts["welcomeLabel"]
        XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 5))
    }
}

Performance Testing

swift
class PerformanceTests: XCTestCase {
    func testJSONParsingPerformance() {
        let jsonData = largeJSONData

        measure {
            _ = try? JSONDecoder().decode([User].self, from: jsonData)
        }
    }

    func testSortingPerformance() {
        let largeArray = Array(1...10000).shuffled()

        measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) {
            _ = largeArray.sorted()
        }
    }
}

Swift Testing Framework (New)

The new testing framework introduced with Swift 6 offers more Swift-like syntax:

swift
import Testing

@Test("Addition should work correctly")
func addition() {
    #expect(MathUtils.add(2, 3) == 5)
}

@Test("Division by zero should throw error")
func divisionByZero() {
    #expect(throws: DivisionError.self) {
        try MathUtils.divide(10, by: 0)
    }
}

@Test("User list should load", arguments: [
    (10, true),
    (0, false),
    (-1, false)
])
func userLoading(count: Int, shouldSucceed: Bool) async throws {
    let service = MockUserService()
    service.userCount = count

    if shouldSucceed {
        let users = try await service.fetchUsers()
        #expect(users.count == count)
    } else {
        #expect(throws: Error.self) {
            try await service.fetchUsers()
        }
    }
}

Test Doubles

swift
// Spy
class SpyUserService: UserServiceProtocol {
    var fetchUsersCalled = false
    var fetchUsersCallCount = 0

    func fetchUsers() async throws -> [User] {
        fetchUsersCalled = true
        fetchUsersCallCount += 1
        return []
    }
}

// Stub
class StubUserService: UserServiceProtocol {
    func fetchUsers() async throws -> [User] {
        return [
            User(id: "1", name: "Test User")
        ]
    }
}

// Fake
class FakeUserService: UserServiceProtocol {
    private var users: [User] = []

    func fetchUsers() async throws -> [User] {
        return users
    }

    func addTestUser(_ user: User) {
        users.append(user)
    }
}

TDD Practices

Red-Green-Refactor

swift
// 1. RED - Write test first
func testCalculateDiscount() {
    let price = 100.0
    let discount = 0.2

    let result = PriceCalculator.calculateDiscount(
        price: price,
        discount: discount
    )

    XCTAssertEqual(result, 80.0, accuracy: 0.01)
}

// 2. GREEN - Write minimum code
struct PriceCalculator {
    static func calculateDiscount(price: Double, discount: Double) -> Double {
        return price * (1 - discount)
    }
}

// 3. REFACTOR - Improve
struct PriceCalculator {
    static func calculateDiscount(
        price: Double,
        discount: Double
    ) -> Double {
        guard price > 0, discount >= 0, discount <= 1 else {
            return price
        }
        return price * (1 - discount)
    }
}

Best Practices

  1. 1Arrange-Act-Assert: Organize tests in three phases
  2. 2Single responsibility: Each test should test one thing
  3. 3Independent: Tests should be independent of each other
  4. 4Fast: Unit tests should run fast
  5. 5Descriptive names: Test names should be descriptive

Testing with Swift is the key to writing reliable and maintainable code!

Related essays

Next essay
Swift Package Manager: Dependency Management