Как тестировать сложную логику внутри view models
Что проверяют
Умеете ли вы спроектировать view model так, чтобы её вообще можно было тестировать, и знаете ли, как проверять асинхронность, таймеры и последовательность состояний. Следом спрашивают, как вы протестируете debounce в поиске или гонку двух запросов.
Как отвечать
Основа: view model не знает про вьюхи, а все зависимости (сеть, хранилище, аналитика, время) приходят через протоколы в инициализатор. В тесте вместо них подставляются фейки с заданными ответами и счётчиками вызовов.
Каркас:
- состояние как значение: удобно держать
enum Stateсо случаямиidle,loading,loaded,failedи проверять переходы, а не десяток разрозненных флагов; - асинхронность: с
async/awaitтест сам становитсяasyncи ждётawait viewModel.load(). Если view model помечена@MainActor, тест тоже помечается@MainActor. Для Combine и@Publishedзначения собирают черезsinkи ждутXCTestExpectation, иначе тест закончится раньше, чем придёт значение; - время: debounce, таймеры и ретраи тестируются через внедрённый
Clockили планировщик, который тест двигает вручную. Настоящийsleepделает тесты медленными и нестабильными; - параметризация: в Swift Testing (Xcode 16 и новее)
@Test(arguments:)прогоняет проверку по таблице входов, утверждения пишутся через#expect; - сценарии: успех, ошибка, пустой ответ, отмена, повторный запрос до завершения первого. Для гонки проверяют, что на экран попал ответ последнего запроса.
Тестируйте поведение через публичный интерфейс: вызвали действие, проверили состояние и то, какие методы фейка вызваны и с какими аргументами. Приватные методы отдельно не тестируются.
Где спотыкаются
Создают зависимости внутри view model через синглтоны. Тест ходит в реальную сеть, и мок подставить некуда.
Не дожидаются асинхронного результата. Тест зелёный, потому что проверка выполнилась до ответа, а баг ловится только случайно.
Проверяют каждый метод по отдельности вместо сценариев. Покрытие высокое, а баг в цепочке «загрузка, ошибка, повтор» не пойман.