XCTest для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

XCTest для Swift: полное руководство с примерами

1. Что это и зачем нужна

XCTest — это стандартный фреймворк для модульного тестирования (unit testing) и UI-тестирования приложений, разработанный компанией Apple. Он встроен непосредственно в Xcode и является основным инструментом для проверки корректности кода на Swift и Objective-C. XCTest предоставляет разработчикам мощный и гибкий набор API для написания, организации и выполнения тестов, начиная от простых проверок отдельных функций и заканчивая сложными интеграционными тестами и тестами пользовательского интерфейса.

Зачем нужен XCTest? В современной разработке программного обеспечения тестирование — это не роскошь, а необходимость. XCTest позволяет автоматизировать проверку кода, что критически важно для поддержания качества приложения в долгосрочной перспективе. С его помощью вы можете:

  • Обнаруживать регрессии: После внесения изменений в код вы можете быстро запустить тесты и убедиться, что новая функциональность не сломала существующую.
  • Документировать код: Хорошо написанные тесты служат живой документацией, показывающей, как именно должна работать та или иная часть системы.
  • Улучшать архитектуру: Стремление сделать код тестируемым часто приводит к более чистой, модульной и слабосвязанной архитектуре.
  • Ускорять разработку: Вместо ручного запуска приложения и выполнения одних и тех же действий снова и снова, вы можете запустить набор тестов одной кнопкой.

XCTest интегрирован с Xcode на всех уровнях: от создания тестовых целей (test targets) до отображения результатов в виде удобных отчетов (Test Navigator). Он поддерживает параллельное выполнение тестов, измерение производительности (performance testing) и асинхронное тестирование, что делает его универсальным решением для проектов любого масштаба.

2. Установка

XCTest не устанавливается через менеджеры пакетов вроде pip или npm. Он является частью экосистемы Apple и поставляется вместе с Xcode. Для его использования не требуется никаких дополнительных команд.

Чтобы начать писать тесты, выполните следующие шаги в Xcode:

  1. Откройте ваш проект.
  2. В меню выберите File → New → Target.
  3. В появившемся окне выберите вкладку Test и затем Unit Testing Bundle (или UI Testing Bundle для UI-тестов).
  4. Нажмите Next, укажите имя цели (например, "YourAppNameTests") и нажмите Finish.

Xcode автоматически создаст для вас файл-шаблон, в который вы сможете добавлять свои тесты. Импортировать XCTest в коде не нужно, так как он автоматически линкуется к тестовой цели.

Для проектов, использующих Swift Package Manager (SPM), XCTest также доступен без дополнительных шагов. При создании пакета с помощью команды swift package init --type executable или --type library, SPM автоматически создаст тестовую директорию Tests с необходимыми файлами.

3. Быстрый старт — минимальный рабочий пример

Рассмотрим простейший пример. Допустим, у нас есть структура Calculator, которую мы хотим протестировать:

// Файл: Sources/Calculator.swift
struct Calculator {
    func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
    
    func subtract(_ a: Int, _ b: Int) -> Int {
        return a - b
    }
}

Теперь создадим тестовый файл (Xcode создаст его автоматически, если вы следовали инструкции выше):

// Файл: Tests/MyProjectTests/CalculatorTests.swift
import XCTest
@testable import MyProject // Замените MyProject на имя вашего модуля

final class CalculatorTests: XCTestCase {
    
    func testAddition() {
        // 1. Arrange (Подготовка)
        let calculator = Calculator()
        
        // 2. Act (Действие)
        let result = calculator.add(2, 3)
        
        // 3. Assert (Проверка)
        XCTAssertEqual(result, 5, "2 + 3 должно быть равно 5")
    }
    
    func testSubtraction() {
        let calculator = Calculator()
        let result = calculator.subtract(10, 4)
        XCTAssertEqual(result, 6, "10 - 4 должно быть равно 6")
    }
}

Чтобы запустить тесты, нажмите на ромбовидную кнопку рядом с объявлением класса или метода, либо используйте комбинацию клавиш Cmd+U. Если тесты пройдут успешно, рядом с ними появятся зеленые галочки.

4. Основные методы/функции/классы

Ниже приведен список ключевых классов и методов XCTest, которые вы будете использовать чаще всего.

4.1 XCTestCase

Сигнатура: class XCTestCase : XCTest

Что делает: Базовый класс для всех тестов. Вы создаете подкласс XCTestCase и добавляете в него свои тестовые методы. Каждый метод, имя которого начинается с test, будет автоматически распознан как отдельный тест.

Пример:

class MyFirstTest: XCTestCase {
    func testExample() {
        XCTAssertTrue(true)
    }
}

4.2 XCTAssertEqual(_:_:_:)

Сигнатура: func XCTAssertEqual<T: Equatable>(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)

Что делает: Проверяет, что два выражения равны (используя протокол Equatable). Если они не равны, тест падает с сообщением об ошибке.

Пример:

func testStringEquality() {
    let name = "Alice"
    XCTAssertEqual(name, "Alice", "Имя должно быть Alice")
}

4.3 XCTAssertTrue(_:_:file:line:) и XCTAssertFalse(_:_:file:line:)

Сигнатура: func XCTAssertTrue(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)

Что делает: XCTAssertTrue проверяет, что булево выражение истинно. XCTAssertFalse проверяет, что оно ложно. Это базовые проверки для любых условий.

Пример:

func testBooleanChecks() {
    let isSunny = true
    XCTAssertTrue(isSunny, "Должно быть солнечно")
    
    let isRaining = false
    XCTAssertFalse(isRaining, "Не должно быть дождя")
}

4.4 XCTAssertNil(_:_:file:line:) и XCTAssertNotNil(_:_:file:line:)

Сигнатура: func XCTAssertNil(_ expression: @autoclosure () throws -> Any?, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)

Что делает: XCTAssertNil проверяет, что выражение равно nil. XCTAssertNotNil проверяет, что выражение не равно nil. Критически важно для работы с опциональными типами.

Пример:

func testOptionalHandling() {
    var user: String? = nil
    XCTAssertNil(user, "Пользователь должен быть nil")
    
    user = "Bob"
    XCTAssertNotNil(user, "Пользователь не должен быть nil")
}

4.5 XCTAssertThrowsError(_:_:file:line:)

Сигнатура: func XCTAssertThrowsError<T>(_ expression: @autoclosure () throws -> T, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line, _ errorHandler: (_ error: Error) -> Void = { _ in })

Что делает: Проверяет, что выражение выбрасывает ошибку. Это основной способ тестирования функций, которые могут завершиться с ошибкой (throws).

Пример:

enum DivisionError: Error {
    case divisionByZero
}

func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else {
        throw DivisionError.divisionByZero
    }
    return a / b
}

func testDivisionByZeroThrows() {
    XCTAssertThrowsError(try divide(10, by: 0)) { error in
        XCTAssertEqual(error as? DivisionError, .divisionByZero)
    }
}

4.6 XCTestExpectation

Сигнатура: class XCTestExpectation : NSObject

Что делает: Используется для тестирования асинхронного кода. Вы создаете ожидание (expectation), выполняете асинхронную операцию, а затем ждете, пока ожидание будет выполнено (fulfilled) в течение заданного таймаута.

Пример:

func testAsyncOperation() {
    let expectation = XCTestExpectation(description: "Асинхронная операция завершена")
    
    DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
        // Симуляция асинхронной работы
        expectation.fulfill() // Сообщаем, что ожидание выполнено
    }
    
    // Ждем выполнения ожидания в течение 1 секунды
    wait(for: [expectation], timeout: 1.0)
}

4.7 setUp() и tearDown()

Сигнатура: override func setUp() и override func tearDown()

Что делает: setUp() вызывается перед каждым тестовым методом в классе. tearDown() вызывается после каждого тестового метода. Идеально подходят для инициализации общих ресурсов (например, создания экземпляров классов) и их последующей очистки.

Пример:

class DatabaseTests: XCTestCase {
    var database: Database!
    
    override func setUp() {
        super.setUp()
        database = Database() // Создаем базу данных перед каждым тестом
    }
    
    override func tearDown() {
        database.close() // Закрываем базу данных после каждого теста
        database = nil
        super.tearDown()
    }
    
    func testInsert() {
        // Используем database, который уже создан в setUp()
        // ...
    }
}

4.8 measure(_:)

Сигнатура: func measure(_ block: () -> Void)

Что делает: Позволяет измерить производительность (время выполнения) блока кода. XCTest запускает блок несколько раз (по умолчанию 10) и собирает статистику. Результаты отображаются в отчете о тестировании.

Пример:

func testSortingPerformance() {
    let largeArray = (1...10000).shuffled()
    measure {
        // Измеряем время сортировки
        _ = largeArray.sorted()
    }
}

5. Реальный практический пример (100-200 строк кода)

Давайте напишем тесты для простого сервиса управления пользователями. Предположим, у нас есть класс UserService, который взаимодействует с сетевым слоем (NetworkService). Мы будем использовать моки (mocks) для изоляции тестов.

// MARK: - Production Code (Sources/MyApp/UserService.swift)

import Foundation

// Протокол для сетевого сервиса, чтобы мы могли его замокать
protocol NetworkServiceProtocol {
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
    func saveUser(_ user: User, completion: @escaping (Bool) -> Void)
}

struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

enum UserServiceError: Error, Equatable {
    case networkError
    case userNotFound
    case invalidData
}

class UserService {
    private let networkService: NetworkServiceProtocol
    
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }
    
    func getUser(id: Int, completion: @escaping (Result<User, UserServiceError>) -> Void) {
        networkService.fetchUser(id: id) { result in
            switch result {
            case .success(let user):
                guard !user.name.isEmpty else {
                    completion(.failure(.invalidData))
                    return
                }
                completion(.success(user))
            case .failure:
                completion(.failure(.networkError))
            }
        }
    }
    
    func createUser(name: String, email: String, completion: @escaping (Bool) -> Void) {
        guard !name.isEmpty, email.contains("@") else {
            completion(false)
            return
        }
        let newUser = User(id: Int.random(in: 1...1000), name: name, email: email)
        networkService.saveUser(newUser, completion: completion)
    }
}

// MARK: - Test Code (Tests/MyAppTests/UserServiceTests.swift)

import XCTest
@testable import MyApp

// Мок для NetworkService
class MockNetworkService: NetworkServiceProtocol {
    var fetchUserResult: Result<User, Error>?
    var saveUserResult: Bool = false
    var savedUser:                    

Рекомендаций