TGViewer
Мобильный трудоголик Мобильный трудоголик @hardworkerit · 1.64K subscribers
Post #291 1.24K
🔢 Как писать стабильные тесты для Swift Concurrency кода.

Тестирование многопоточного кода в Swift всегда было не без проблем. С появлением Swift Concurrency ситуация стала лучше, но не идеальной. Самые проблемные - плавающие тесты, которые то проходят, то падают без видимых причин. Корень проблемы в недетерминированном выполнении асинхронных операций, особенно при использовании Task {}. Сейчас разберем, как заставить такие тесты работать предсказуемо.


Почему обычные тесты с Task не работают:

Когда мы создаем Task внутри синхронного метода, тест не ждет его завершения:

// Плавающий тест
func testAsyncOperation() {
let service = DataService()
service.startProcessing() // Внутри создается Task

XCTAssertTrue(service.isCompleted) // Может упасть
}


Тест завершится раньше, чем Task выполнится, потому что Task {} запускает асинхронную операцию, которая живет своей жизнью.


Решение - абстракция над Task:

Ключевая идея - создать протокол TaskProvider, который инкапсулирует создание задач:

protocol TaskProvider {
func task<T>(priority: TaskPriority?, operation: @escaping () async -> T) -> Task<T, Never>
}

struct DefaultTaskProvider: TaskProvider {
func task<T>(priority: TaskPriority?, operation: @escaping () async -> T) -> Task<T, Never> {
Task(priority: priority, operation: operation)
}
}

// Мок для тестов
class MockTaskProvider: TaskProvider {
private var tasks: [Task<Void, Never>] = []

func task<T>(priority: TaskPriority?, operation: @escaping () async -> T) -> Task<T, Never> {
let task = Task(priority: priority) {
await operation()
}

tasks.append(task as! Task<Void, Never>)
return task
}

// Ждем завершения всех созданных задач
func waitForAllTasks() async {
for task in tasks {
await task.value
}
}
}



Использование в коде:

Меняем зависимость в нашем сервисе:

class DataService {
private let taskProvider: TaskProvider

init(taskProvider: TaskProvider = DefaultTaskProvider()) {
self.taskProvider = taskProvider
}

func startProcessing() {
taskProvider.task(priority: .medium) {
// Долгая асинхронная операция
await self.processData()
}
}
}



Стабильный тест:

Теперь тест может дождаться выполнения всех задач:

func testAsyncOperation() async {
let mockProvider = MockTaskProvider()
let service = DataService(taskProvider: mockProvider)

service.startProcessing()
await mockProvider.waitForAllTasks() // Ждем завершения

XCTAssertTrue(service.isCompleted) // Стабильно проходит
}



Преимущества подхода:


🔵Контроль приоритетов: можно проверять, что задачи создаются с правильными приоритетами.

🔵Отслеживание создания: можно вести лог созданных задач для assertions.

🔵Изоляция тестов: каждый тест работает с изолированным моком.

🔵Поддержка detached задач: паттерн легко расширяется для Task.detached.


Важное замечание:

Этот подход не заменяет async/await тесты, а дополняет их. Для кода, который уже использует async функции, лучше тестировать через await. Но для legacy-кода или ситуаций, где Task создается внутри синхронных методов, этот паттерн незаменим.


🔗 Ссылка на подробную статью


💡 Вывод:

Тестирование многопоточного кода требует особого подхода и абстракция TaskProvider предоставляет его. Она превращает плавающие тесты в детерминированные, давая полный контроль над выполнением асинхронных операций. Через инверсию зависимостей сохраняется чистота архитектуры, а случайные падения в CI/CD становятся историей.

Особенно ценен этот паттерн при работе с legacy-кодом, где синхронные и асинхронные вызовы соседствуют в процессе миграции на Swift Concurrency.


➡️ Подписаться на канал
Мобильный трудоголик
  • 👍 11
  • 🔥 4
  • ❤ 1
  • 🙏 1
  • 🤝 1
More from @hardworkerit
  1. Oct 4, 2026🔢 SwiftUI: определение угла раскрытия iPhone Duo. В iOS 27.1 появился модификатор .onHing…
  2. Oct 2, 2026👣 Архитекторы, тестировщики и кодеры. Создаем мультиагентную команду Разработка с ИИ-аген…
  3. Oct 1, 2026🔢 Работа с Picture-in-Picture в iOS. Picture-in-Picture - системная функция iOS, которая…
  4. Sep 29, 2026🔢 Управление тулбарами на iPhone Duo. На iPhone Duo элементы управления навигацией, дейст…
  5. Sep 27, 2026🔢 Новые возможности Hashable в Swift 6.4 В Swift 6.4 добавили Hashable для нескольких тип…
  6. Sep 25, 2026🔢 Приватные свойства больше не ломают memberwise инициализатор в Swift 6.4 Swift автомати…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →