Тестирование в Swift: unit, integration, e2e от А до Я

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

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

Начать курс

Введение: Почему тестирование — основа надежного Swift-приложения

Разработка под iOS и macOS требует не только красивого интерфейса, но и стабильной логики. Тестирование в Swift — это не опция, а необходимость. Без тестов даже небольшое изменение может сломать функционал, который работал годами. В этой статье мы разберем три уровня тестирования: unit (модульное), integration (интеграционное) и e2e (сквозное). Вы узнаете, как их правильно настраивать, писать и запускать на платформах Apple. В конце — практические советы по организации тестовой архитектуры.

Уровень 1: Unit-тесты — основа основ

Unit-тесты проверяют отдельные функции, методы или классы изолированно. В Swift основной фреймворк — XCTest. Он встроен в Xcode, не требует сторонних зависимоций и идеально подходит для тестирования бизнес-логики.

Как написать первый unit-тест

Создайте новый Unit Test Target в Xcode (File → New → Target → Unit Testing Bundle). Затем добавьте тестовый класс:

import XCTest
@testable import MyApp

final class CalculatorTests: XCTestCase {
    
    var sut: Calculator!
    
    override func setUpWithError() throws {
        sut = Calculator()
    }
    
    override func tearDownWithError() throws {
        sut = nil
    }
    
    func testAddition() {
        // Given
        let a = 5
        let b = 3
        
        // When
        let result = sut.add(a, b)
        
        // Then
        XCTAssertEqual(result, 8, "Сложение должно быть корректным")
    }
}

Лучшие практики unit-тестирования

  • Изоляция: каждый тест тестирует только одну единицу кода. Используйте setUp и tearDown для сброса состояния.
  • Именование: используйте шаблон test[Method]_[Condition]_[ExpectedResult].
  • Моки и стабы: для внешних зависимостей (сеть, база данных) используйте протоколы и подменяйте их в тестах.
  • Покрытие: стремитесь к 70-80% покрытия кода, но не гонитесь за 100% — тестируйте критичные пути.

Пример с моком

protocol NetworkServiceProtocol {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}

class MockNetworkService: NetworkServiceProtocol {
    var shouldReturnError = false
    
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        if shouldReturnError {
            completion(.failure(NSError(domain: "test", code: 1)))
        } else {
            completion(.success(Data()))
        }
    }
}

func testFetchDataSuccess() {
    let mock = MockNetworkService()
    let viewModel = ViewModel(networkService: mock)
    
    mock.shouldReturnError = false
    viewModel.loadData()
    XCTAssertTrue(viewModel.isDataLoaded)
}

Уровень 2: Интеграционное тестирование — проверка взаимодействия

Интеграционные тесты проверяют, как разные модули системы работают вместе: база данных + сеть, UI + бизнес-логика, несколько сервисов. В Swift для этого используют XCTest с реальными (или in-memory) зависимостями.

Типичный сценарий: Core Data + Network

import XCTest
@testable import MyApp

class UserServiceIntegrationTests: XCTestCase {
    
    var coreDataStack: CoreDataStack!
    var networkService: NetworkService!
    var userService: UserService!
    
    override func setUp() {
        coreDataStack = CoreDataStack(inMemory: true) // in-memory для тестов
        networkService = NetworkService()
        userService = UserService(coreDataStack: coreDataStack, networkService: networkService)
    }
    
    func testSaveUserFromNetwork() {
        // Создаем ожидание для асинхронного запроса
        let expectation = self.expectation(description: "Save user")
        
        userService.fetchAndSaveUser(id: 123) { result in
            switch result {
            case .success:
                // Проверяем, что пользователь сохранился в Core Data
                let users = self.coreDataStack.fetchUsers()
                XCTAssertEqual(users.count, 1)
                XCTAssertEqual(users.first?.id, 123)
            case .failure:
                XCTFail("Сохранение не удалось")
            }
            expectation.fulfill()
        }
        
        waitForExpectations(timeout: 5)
    }
}

Когда использовать интеграционные тесты

  • Критичные бизнес-процессы (авторизация, платежи).
  • Сложные взаимодействия между модулями.
  • Проверка работы с реальной базой данных (SQLite, Core Data, Realm).
  • Тестирование API-клиентов с реальным сервером (или локальным mock-сервером).

Совет: не смешивайте unit и интеграционные тесты в одном классе. Выносите их в отдельные target'ы или группы, чтобы можно было запускать быстро (unit) и медленно (integration) отдельно.

Уровень 3: E2E-тестирование — симуляция пользователя

End-to-end (E2E) тесты проверяют приложение целиком, как это делает настоящий пользователь. В Swift используется XCUITest — фреймворк для автоматизации UI. Тесты запускаются на симуляторе или реальном устройстве.

Пишем первый E2E-тест

import XCTest

class LoginFlowUITests: XCTestCase {
    
    var app: XCUIApplication!
    
    override func setUp() {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }
    
    func testSuccessfulLogin() {
        // Находим элементы по accessibility identifier
        let usernameField = app.textFields["usernameInput"]
        let passwordField = app.secureTextFields["passwordInput"]
        let loginButton = app.buttons["loginButton"]
        
        // Действия пользователя
        usernameField.tap()
        usernameField.typeText("test_user")
        
        passwordField.tap()
        passwordField.typeText("password123")
        
        loginButton.tap()
        
        // Проверка результата
        let welcomeLabel = app.staticTexts["welcomeLabel"]
        XCTAssertTrue(welcomeLabel.exists, "Пользователь должен быть перенаправлен на главный экран")
        XCTAssertEqual(welcomeLabel.label, "Добро пожаловать, test_user!")
    }
}

Настройка accessibility для тестов

Чтобы XCUITest мог находить элементы, их нужно правильно маркировать:

// SwiftUI
TextField("Логин", text:                

Блоги

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