Розробка 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+ проєктах. Пишіть нам для безкоштовної оцінки — і ваш код стане надійнішим.







