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

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

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

Начать курс

Продвинутое тестирование в Swift: Unit, Integration и E2E

Тестирование — это не просто проверка, а фундамент надежного кода. На продвинутом уровне разработчику Swift нужно не просто писать тесты, а грамотно выстраивать пирамиду тестирования: от быстрых unit-тестов до сложных end-to-end (e2e) сценариев. В этой статье мы разберем продвинутые техники для iOS/macOS-проектов: работу с зависимостями, интеграционные тесты CoreData и автоматизацию UI.

1. Unit-тесты: управление зависимостями и моки

На продвинутом уровне unit-тесты должны быть изолированы. Ключевой прием — Dependency Injection (DI). Вместо того чтобы тестировать реальный сетевой слой или базу данных, мы подменяем их моками.

Пример: протокол и DI

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

class ViewModel {
    private let networkService: NetworkServiceProtocol
    
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }
    
    func loadData() {
        networkService.fetchData { result in
            // обработка
        }
    }
}

// Мок для тестов
class MockNetworkService: NetworkServiceProtocol {
    var shouldReturnError = false
    
    func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
        if shouldReturnError {
            completion(.failure(NSError(domain: "test", code: -1)))
        } else {
            completion(.success("Mock data"))
        }
    }
}

В тесте вы создаете MockNetworkService и передаете его в ViewModel. Это позволяет проверять сценарии ошибок и успеха без реального API.

XCTestExpectation для асинхронности

func testViewModelLoadDataSuccess() {
    let mock = MockNetworkService()
    let viewModel = ViewModel(networkService: mock)
    
    let expectation = self.expectation(description: "Data loaded")
    
    viewModel.loadData()
    
    // Симулируем асинхронный вызов
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        expectation.fulfill()
    }
    
    waitForExpectations(timeout: 1.0)
    // Проверяем состояние ViewModel
}

2. Интеграционные тесты: CoreData и работа с реальными данными

Интеграционные тесты проверяют взаимодействие компонентов. Для CoreData мы используем in-memory контейнер, чтобы не загрязнять основную базу.

Настройка in-memory CoreData

class CoreDataTestStack {
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "MyModel")
        let description = NSPersistentStoreDescription()
        description.type = NSInMemoryStoreType
        container.persistentStoreDescriptions = [description]
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Failed to load store: \(error)")
            }
        }
        return container
    }()
    
    var context: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
}

Пример интеграционного теста

func testSaveAndFetchUser() {
    let stack = CoreDataTestStack()
    let repository = UserRepository(context: stack.context)
    
    // Создаем пользователя
    let user = User(context: stack.context)
    user.name = "Test User"
    user.age = 30
    
    try? stack.context.save()
    
    // Проверяем, что данные сохранились
    let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
    let users = try? stack.context.fetch(fetchRequest)
    
    XCTAssertEqual(users?.count, 1)
    XCTAssertEqual(users?.first?.name, "Test User")
}

Важно: после каждого теста очищайте контекст, чтобы тесты не влияли друг на друга.

3. E2E-тесты на XCUITest: сценарии и ожидания

End-to-end тесты имитируют пользовательские действия. На продвинутом уровне мы используем кастомные ожидания и Page Object Pattern.

Page Object Pattern

class LoginPage {
    let app: XCUIApplication
    
    init(app: XCUIApplication) {
        self.app = app
    }
    
    var loginTextField: XCUIElement {
        return app.textFields["loginField"]
    }
    
    var passwordTextField: XCUIElement {
        return app.secureTextFields["passwordField"]
    }
    
    var loginButton: XCUIElement {
        return app.buttons["loginButton"]
    }
    
    func login(username: String, password: String) {
        loginTextField.tap()
        loginTextField.typeText(username)
        passwordTextField.tap()
        passwordTextField.typeText(password)
        loginButton.tap()
    }
}

Тест с ожиданием результата

func testSuccessfulLogin() {
    let app = XCUIApplication()
    app.launch()
    
    let loginPage = LoginPage(app: app)
    loginPage.login(username: "test@example.com", password: "password123")
    
    // Ожидаем появления экрана профиля
    let profileLabel = app.staticTexts["profileLabel"]
    let exists = profileLabel.waitForExistence(timeout: 5)
    
    XCTAssertTrue(exists, "Профиль не появился после логина")
}

Совет: используйте accessibilityIdentifier для элементов UI, чтобы тесты не ломались при изменении текста.

4. Продвинутые техники: Code Coverage и параллельный запуск

Настройте Code Coverage в Xcode (Product → Scheme → Edit Scheme → Test → Options → Gather coverage data). Стремитесь к покрытию 80%+ для критических модулей.

Для ускорения тестов используйте параллельный запуск в Xcode Cloud или CI/CD. Разделите тесты на группы: unit (быстрые, без UI) и UI (медленные).

Пример настройки fastlane для параллельного запуска:

lane :tests do
  scan(
    scheme: "MyApp",
    parallel_testrun_count: 4,
    code_coverage: true
  )
end

Заключение

Продвинутое тестирование в Swift — это комбинация правильной архитектуры (DI, протоколы), изолированных тестов (in-memory CoreData) и автоматизации UI (Page Object). Внедряйте эти практики постепенно: начните с unit-тестов на критическую логику, добавьте интеграционные для CoreData и завершите e2e для ключевых пользовательских сценариев. Помните, что тесты — это инвестиция в стабильность приложения и скорость разработки.

Блоги

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