Розробка 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 — unit-тести в 6,7 раза швидші.
// 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 днів (у середньому 4 дні) залежно від обсягу кодової бази та поточного рівня архітектурної ізольованості компонентів. Вартість впровадження — від $500 до $1500.
Ми займаємося iOS-розробкою понад 5 років (5+ років досвіду), впровадили unit-тести у 30+ проєктах. Пишіть нам для оцінки — і ваш код стане надійнішим.







