На middle и senior собесах спрашивают не только «как писать», но и «как измерить и проверить». Здесь инструменты, метрики и стратегия тестов.
Что измерять
- Запуск: время до первого кадра. Холодный старт (процесса нет) самый дорогой.
- Плавность: рывки (hitches), когда кадр не уложился в бюджет. 60 Гц дают 16,7 мс на кадр, 120 Гц на ProMotion 8,3 мс.
- Зависания (hangs): главный поток не отвечает на ввод заметное время (от ~250 мс).
- Память: пиковое потребление и рост. При превышении лимита система убивает процесс (jetsam), и в обычных крэш-репортах этого не видно.
- Батарея и сеть: фоновая активность, лишние запросы.
Измеряют на Release-сборке на реальном, лучше старом устройстве, а не в симуляторе.
Instruments
| Шаблон | Вопрос |
|---|---|
| Time Profiler | где тратится процессорное время |
| Animation Hitches | какие кадры опоздали и почему |
| App Launch | что происходит до первого кадра |
| Allocations | какие объекты растут |
| Leaks | утечки памяти |
| Network | какие запросы и сколько данных |
В поле данные собирает MetricKit: время запуска, hang rate, память, диагностику крэшей и зависаний со стеками. Похожие сводки есть в Xcode Organizer.
Типичные проблемы и решения
Картинки. Память декодированной картинки это ширина × высота × 4 байта, независимо от размера файла: фото 4000×3000 занимает 48 МБ. Картинки уменьшают до размера показа (downsampling через ImageIO), декодируют в фоне и держат в NSCache.
Запуск. До main работает загрузчик: каждая динамическая библиотека стоит времени, поэтому зависимости линкуют статически. После main в didFinishLaunching и до первого кадра делают только нужное первому экрану, SDK инициализируют лениво, первый экран показывают из кеша.
Списки. Никакой синхронной работы в конфигурации ячейки, тени с shadowPath, prefetching, точечные обновления вместо reloadData.
Гонки данных. Thread Sanitizer в схеме тестов ловит одновременный доступ без синхронизации. Main Thread Checker ловит обращения к UI не с главного потока. Режим Swift 6 находит многие гонки ещё при компиляции.
Тестирование
Пирамида: много юнит-тестов, меньше интеграционных, немного UI-тестов на ключевые сценарии.
Тестируемость закладывается архитектурой. View model получает зависимости через инициализатор и протоколы:
protocol ProfileService { func load() async throws -> Profile }
@MainActor final class ProfileViewModel {
private let service: ProfileService
init(service: ProfileService) { self.service = service }
}
Тестовые двойники:
- стаб возвращает заготовленные ответы;
- мок или спай записывает вызовы для проверки взаимодействия;
- фейк это упрощённая рабочая реализация (база в памяти);
- dummy только заполняет параметр.
Swift Testing (Xcode 16+):
import Testing
@Test func totalWithDiscount() {
let cart = Cart(items: [.init(price: 1000)], discount: 10)
#expect(cart.total == 900)
}
@Test(arguments: [0, 10, 50])
func discountNeverNegative(percent: Int) {
#expect(Cart(items: [], discount: percent).total >= 0)
}
#expect показывает значения при провале, #require прерывает тест, тесты по умолчанию параллельны. XCTest остаётся для UI-тестов и старого кода, async-тесты пишутся как func test() async throws.
Время в тестах. Вместо реального ожидания внедряют Clock и в тесте продвигают время вручную: тест быстрый и стабильный.
Сеть. Подкласс URLProtocol перехватывает запросы тестовой сессии и отдаёт заготовленные ответы, проверяя весь путь от запроса до декодирования.
UI-тесты (XCUITest) работают в отдельном процессе через дерево доступности. Элементы ищут по accessibilityIdentifier, а не по тексту. Ими покрывают только критичные пути: вход, покупку, онбординг.
Snapshot-тесты сравнивают скриншоты компонентов с эталоном, полезны для дизайн-системы.
Утечки в тестах: слабая ссылка на объект и проверка в teardown, что она стала nil.
Частые ошибки
- Профилировать Debug-сборку в симуляторе.
- Ждать в тестах реальные секунды.
- Искать элементы UI-тестов по тексту.
- Перезапускать нестабильные тесты вместо исправления.
Что спросят на собесе
- Как найти, почему экран медленно открывается?
- Как ускорить холодный старт?
- Сколько памяти занимает картинка и как это уменьшить?
- Как вы тестируете view model, сеть, код со временем?
- Чем Swift Testing отличается от XCTest?