Тестирование в Swift: Unit, Integration, E2E — полное руководство

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

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

Начать курс

Тестирование в Swift: Unit, Integration, E2E — от теории к практике

Качественное тестирование — это не просто «хороший тон» в разработке под iOS и macOS, а критически важный этап, обеспечивающий стабильность, безопасность и предсказуемость приложения. В этой статье мы разберём три уровня тестирования на Swift: Unit-тесты, интеграционные тесты и E2E (end-to-end). Вы узнаете, чем они отличаются, когда их применять и как писать эффективные тесты с использованием фреймворков XCTest и XCUITest.

1. Unit-тестирование: проверяем «кирпичики» приложения

Unit-тесты — это самый низкоуровневый и самый быстрый вид тестов. Они проверяют работу отдельных функций, методов или классов (юнитов) в изоляции от внешних зависимостей (сеть, база данных, UI).

Основные принципы Unit-тестов в Swift:

  • Изоляция: тестируемый код не должен зависеть от состояния других модулей.
  • Повторяемость: тест должен давать одинаковый результат при каждом запуске.
  • Быстрота: тысячи unit-тестов должны выполняться за секунды.

Пример Unit-теста с XCTest:

import XCTest

class Calculator {
    func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
    
    func divide(_ a: Int, _ b: Int) throws -> Int {
        guard b != 0 else { throw CalculationError.divisionByZero }
        return a / b
    }
}

enum CalculationError: Error {
    case divisionByZero
}

class CalculatorTests: XCTestCase {
    var calculator: Calculator!
    
    override func setUp() {
        super.setUp()
        calculator = Calculator()
    }
    
    override func tearDown() {
        calculator = nil
        super.tearDown()
    }
    
    func testAddition() {
        // Given
        let a = 5
        let b = 3
        
        // When
        let result = calculator.add(a, b)
        
        // Then
        XCTAssertEqual(result, 8, "5 + 3 должно быть равно 8")
    }
    
    func testDivisionByZeroThrowsError() {
        // Используем XCTAssertThrowsError для проверки исключений
        XCTAssertThrowsError(try calculator.divide(10, 0)) { error in
            XCTAssertEqual(error as? CalculationError, .divisionByZero)
        }
    }
}

Лучшие практики для Unit-тестов:

  • Используйте шаблон AAA (Arrange-Act-Assert) — подготовка, действие, проверка.
  • Называйте тесты осмысленно: testMethodName_condition_expectedResult.
  • Избегайте тестирования private методов — тестируйте публичный интерфейс.
  • Для моков и стабов используйте протоколы и dependency injection.

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

Интеграционные тесты проверяют, как различные модули приложения работают вместе. В контексте iOS это может быть взаимодействие ViewModel с сетью, Core Data или UserDefaults.

Пример интеграционного теста с моком сетевого слоя:

import XCTest

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

// Реальный сервис (не тестируем его в интеграционном тесте)
class RealNetworkService: NetworkServiceProtocol {
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
        // Реальный HTTP-запрос
    }
}

// Mock-сервис для тестирования
class MockNetworkService: NetworkServiceProtocol {
    var shouldReturnError = false
    var mockUser = User(id: 1, name: "Test User")
    
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
        if shouldReturnError {
            completion(.failure(NSError(domain: "test", code: 500)))
        } else {
            completion(.success(mockUser))
        }
    }
}

struct User: Codable, Equatable {
    let id: Int
    let name: String
}

class UserViewModel {
    private let networkService: NetworkServiceProtocol
    var userName: String?
    var errorMessage: String?
    
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }
    
    func loadUser(id: Int) {
        networkService.fetchUser(id: id) { [weak self] result in
            switch result {
            case .success(let user):
                self?.userName = user.name
            case .failure(let error):
                self?.errorMessage = error.localizedDescription
            }
        }
    }
}

class UserViewModelIntegrationTests: XCTestCase {
    func testLoadUserSuccess() {
        // Arrange
        let mockService = MockNetworkService()
        let viewModel = UserViewModel(networkService: mockService)
        
        // Act
        viewModel.loadUser(id: 1)
        
        // Assert
        XCTAssertEqual(viewModel.userName, "Test User")
        XCTAssertNil(viewModel.errorMessage)
    }
    
    func testLoadUserFailure() {
        // Arrange
        let mockService = MockNetworkService()
        mockService.shouldReturnError = true
        let viewModel = UserViewModel(networkService: mockService)
        
        // Act
        viewModel.loadUser(id: 1)
        
        // Assert
        XCTAssertNil(viewModel.userName)
        XCTAssertNotNil(viewModel.errorMessage)
    }
}

Когда писать интеграционные тесты?

  • При работе с базами данных (Core Data, Realm).
  • При интеграции с REST API или WebSocket.
  • При использовании сторонних SDK (Firebase, Alamofire).
  • Для проверки корректности DI-контейнеров.

3. E2E-тестирование: проверяем приложение целиком

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

Пример E2E-теста с XCUITest:

import XCTest

class LoginFlowUITests: XCTestCase {
    var app: XCUIApplication!
    
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }
    
    func testSuccessfulLogin() {
        // Находим элементы по accessibility identifier
        let usernameField = app.textFields["usernameTextField"]
        let passwordField = app.secureTextFields["passwordTextField"]
        let loginButton = app.buttons["loginButton"]
        let welcomeLabel = app.staticTexts["welcomeLabel"]
        
        // Симулируем ввод данных
        usernameField.tap()
        usernameField.typeText("testuser")                

Блоги

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