Разработка Unit-тестов для iOS-приложения (XCTest)
Представьте: ViewModel разрослась до 700 строк, в ней смешана бизнес-логика, форматирование данных и прямые вызовы сети. Добавляете новую фичу — падает старый поток. Откатываете — снова работает, но причина неясна. Именно в такой ситуации мы и пишем unit-тесты на XCTest. Наш подход — не гнаться за 100% покрытием, а обеспечить возможность рефакторить без страха. Мы тестируем только то, что может сломаться: бизнес-логику, граничные случаи, асинхронные операции. За годы работы над iOS-проектами мы выполнили более 30 внедрений unit-тестов для клиентов из разных сфер — от финтеха до e-commerce. Мы гарантируем стабильность тестов в CI и прозрачный отчёт по покрытию. В этой статье расскажем, как именно мы это делаем и почему XCTest — лучший выбор для iOS.
Какие unit-тесты нужно писать в первую очередь?
Бизнес-логика — главный приоритет. ViewModel, Interactor, UseCase — всё, где есть ветвления if/switch, вычисления, трансформации данных. Протокол-ориентированный подход Swift делает это удобным: зависимости инжектируются через протоколы, в тестах подменяются mock-объектами.
// Протокол сервиса protocol UserServiceProtocol { func fetchUser(id: String) async throws -> User } // Mock для тестов class MockUserService: UserServiceProtocol { var stubbedUser: User? var stubbedError: Error? func fetchUser(id: String) async throws -> User { if let error = stubbedError { throw error } return stubbedUser! } } // Тест func testFetchUserSuccess() async throws { let mockService = MockUserService() mockService.stubbedUser = User(id: "1", name: "Test") let sut = UserViewModel(service: mockService) await sut.loadUser(id: "1") XCTAssertEqual(sut.user?.name, "Test") XCTAssertFalse(sut.isLoading) } Как XCTest помогает тестировать асинхронный код?
Современный Swift с async/await тестируется нативно: XCTest поддерживает async тест-функции с iOS 15+ и Xcode 13+. Для Combine-based кода — XCTestExpectation + sink. Мы используем оба подхода в зависимости от архитектуры. В одном из проектов мы заменили 40% медленных интеграционных тестов на unit-тесты с моками, сократив время прогона с 20 минут до 3.
// Combine: тестируем Publisher func testPublisherEmitsValue() { let expectation = expectation(description: "Value received") var cancellables = Set<AnyCancellable>() sut.statePublisher .dropFirst() // пропускаем начальное состояние .sink { state in XCTAssertEqual(state, .loaded) expectation.fulfill() } .store(in: &cancellables) sut.loadData() waitForExpectations(timeout: 2) } Граничные случаи — то, что реально падает в продакшене: пустой массив, nil-значение, строка с юникодом, дата в другом timezone. Не только happy path, а именно edge cases.
Архитектура, удобная для тестирования
XCTest-тесты пишутся просто, когда архитектура предполагает инверсию зависимостей. MVVM с DI через инициализатор, Clean Architecture с UseCase — тестируются прямо. Singleton-ы и статические методы — нет. Если проект не использует DI, часть работы — рефакторинг перед написанием тестов.
Частая проблема: ViewModel обращается к UserDefaults напрямую, к Date() напрямую. Оба нужно обернуть в протоколы и инжектировать — иначе тесты будут зависеть от системного состояния и времени запуска.
Например, в проекте по доставке еды мы переписали ViewModel на MVVM с DI, после чего покрытие тестами достигло 85%, а время регрессии сократилось вдвое.
Типичные ошибки в iOS unit-тестах
-
@testable importбез флага-enable-testingв build settings — импорт не работает в CI - Тесты, зависящие от порядка — XCTest не гарантирует порядок выполнения, каждый тест должен быть изолирован через setUp()/tearDown()
- Реальные сетевые запросы в тестах — тест становится нестабильным и медленным. Всегда мокируем через URLProtocol subclass или URLSession с кастомным URLSessionConfiguration
Сравнение типов тестов
| Тип теста | Что проверяет | Скорость | Стабильность |
|---|---|---|---|
| Unit (XCTest) | Бизнес-логика, методы | Секунды | 100% |
| Интеграционные | Взаимодействие модулей | Минуты | Зависит от окружения |
| UI (XCUITest) | Пользовательские сценарии | Минуты | 80-90% |
Unit-тесты на XCTest дают лучшее соотношение скорость/надёжность. Наши проекты достигают 80% покрытия бизнес-логики, что сокращает время регрессионного тестирования на 40%. Unit-тесты на XCTest в 5 раз быстрее интеграционных тестов для проверки бизнес-логики.
Таблица типичных проблем и решений
| Проблема | Последствие | Решение |
|---|---|---|
| Нет инверсии зависимостей | Невозможно подменить сервис | Инжекция через протоколы |
| Использование синглтонов | Тесты не изолированы | Замена на DI-провайдер |
| Тесты зависят от времени | Плавающие результаты | Инжекция Date() через протокол |
Что входит в работу
- Аудит существующего кода и архитектуры
- Выделение компонентов для тестирования (минимальный рефакторинг для DI)
- Написание unit-тестов на XCTest с моками и стабами
- Интеграция в CI (GitHub Actions, Bitrise, GitLab CI)
- Отчёт по покрытию (Xcode Coverage Report, xcov)
- Рекомендации по поддержке тестов
Процесс работы
Аудит существующего кода → выделение тестируемых компонентов → при необходимости минимальный рефакторинг для DI → написание тестов → интеграция в CI. Отчёт по покрытию после завершения.
Срок: 3–5 дней в зависимости от объёма кодовой базы и текущего уровня архитектурной изолированности компонентов.
Мы занимаемся iOS-разработкой более 5 лет, внедрили unit-тесты в 30+ проектах. Закажите внедрение unit-тестов — и ваш код станет надёжнее.







