Тестирование в Swift: unit, integration, e2e — полное руководство

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

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

Начать курс

Введение: Зачем тестировать Swift-приложения?

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

В этой статье мы разберём, как правильно настроить и написать тесты для Swift-приложений, используя современные практики и инструменты. Вы узнаете, чем отличаются разные типы тестов, и получите готовые примеры кода, которые можно сразу применить в своём проекте.

Unit-тестирование в Swift

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

Настройка Unit-тестов в Xcode

Для создания тестового таргета в Xcode, при создании проекта достаточно отметить галочку "Include Tests". Если проект уже существует, добавьте новый таргет: File → New → Target → iOS Unit Testing Bundle.

Базовый пример Unit-теста

Рассмотрим простой пример тестирования калькулятора:

import XCTest

final class CalculatorTests: XCTestCase {
    
    var calculator: Calculator!
    
    override func setUpWithError() throws {
        calculator = Calculator()
    }
    
    override func tearDownWithError() throws {
        calculator = nil
    }
    
    func testAddition() {
        // Given
        let a = 5
        let b = 3
        
        // When
        let result = calculator.add(a, b)
        
        // Then
        XCTAssertEqual(result, 8, "5 + 3 должно быть равно 8")
    }
    
    func testDivisionByZero() {
        XCTAssertThrowsError(try calculator.divide(10, 0)) { error in
            XCTAssertEqual(error as? CalculatorError, CalculatorError.divisionByZero)
        }
    }
}

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

  • Изоляция: Каждый тест должен быть независим. Используйте setUpWithError и tearDownWithError для подготовки и очистки.
  • Паттерн AAA: Arrange (подготовка), Act (действие), Assert (проверка).
  • Одна проверка на тест: Идеальный тест проверяет только одно поведение.
  • Именование: Используйте описательные имена: testMethodName_WhenCondition_ExpectedResult.

Интеграционное тестирование в Swift

Интеграционные тесты проверяют взаимодействие между компонентами: работа с базой данных, сетевыми запросами, файловой системой. В отличие от unit-тестов, здесь мы не мокаем всё подряд, а тестируем реальные взаимодействия.

Тестирование Core Data

Пример тестирования работы с Core Data:

import XCTest
@testable import MyApp

final class CoreDataIntegrationTests: XCTestCase {
    
    var coreDataStack: CoreDataStack!
    
    override func setUpWithError() throws {
        coreDataStack = CoreDataStack(inMemory: true) // Используем in-memory хранилище
    }
    
    func testSaveAndFetchUser() {
        // Arrange
        let user = User(context: coreDataStack.viewContext)
        user.name = "Иван"
        user.age = 30
        
        // Act
        try? coreDataStack.viewContext.save()
        
        let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
        let users = try? coreDataStack.viewContext.fetch(fetchRequest)
        
        // Assert
        XCTAssertEqual(users?.count, 1)
        XCTAssertEqual(users?.first?.name, "Иван")
    }
}

Тестирование сетевого слоя

Для интеграционных тестов сети используйте реальные запросы к тестовому серверу или URLProtocol для перехвата запросов:

class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
    
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            XCTFail("Handler not set")
            return
        }
        
        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }
    
    override func stopLoading() {}
}

E2E-тестирование (XCUITest)

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

Настройка E2E-тестов

Добавьте новый таргет: File → New → Target → iOS UI Testing Bundle. XCUITest запускает приложение на симуляторе или реальном устройстве.

Пример UI-теста для логина

import XCTest

final class LoginUITests: XCTestCase {
    
    var app: XCUIApplication!
    
    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }
    
    func testSuccessfulLogin() {
        // Находим элементы интерфейса
        let emailTextField = app.textFields["emailField"]
        let passwordTextField = app.secureTextFields["passwordField"]
        let loginButton = app.buttons["loginButton"]
        
        // Действия пользователя
        emailTextField.tap()
        emailTextField.typeText("test@example.com")
        
        passwordTextField.tap()
        passwordTextField.typeText("password123")
        
        loginButton.tap()
        
        // Проверяем результат
        let welcomeLabel = app.staticTexts["welcomeLabel"]
        XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 5))
        XCTAssertEqual(welcomeLabel.label, "Добро пожаловать!")
    }
    
    func testInvalidEmailError() {
        let emailTextField = app.textFields["emailField"]
        let loginButton = app.buttons["loginButton"]
        
        emailTextField.tap()
        emailTextField.typeText("invalid")
        
        loginButton.tap()
        
        let errorLabel = app.staticTexts["errorLabel"]
        XCTAssertTrue(errorLabel.exists)
        XCTAssertEqual(errorLabel.label, "Некорректный email")
    }
}

Советы по XCUITest