Добавить объявление

XCTest уходит в прошлое? Разбираем плюсы и минусы Swift Testing

С выходом Swift Testing многие задумались о миграции с XCTest. Но так ли всё гладко, как обещает Apple? Давайте разберем реальные подводные камни и лайфхаки для перехода.

Чем Swift Testing отличается от XCTest:


Обнаружение тестов:


  • XCTest: использует XCTestCase + Objective-C runtime.

  • Swift Testing: макросы @Test + Swift ориентированный подход.

Выполнение тестов:


  • XCTest: выполняется последовательно (один за другим).

  • Swift Testing: выполняется параллельно (каждый тест в отдельной Task).

Синтаксис:


Разница в коде:

// XCTest
func testLogin() { XCTAssertEqual(result, expected) }

// Swift Testing
@Test func login() { #expect(result == expected) }

Главные проблемы миграции:



Ассерты вне контекста Task:

Swift Testing требует, чтобы проверки (#expect) выполнялись только внутри Task.

// Старая схема (XCTest)
func testAsync() {
DispatchQueue.main.async {
XCTAssertEqual(result, 42)
}
}

// Неправильная миграция (упадёт в Swift Testing)
@Test func asyncTest() {
DispatchQueue.main.async {
#expect(result == 42) // Ошибка: вне контекста Task
}
}

// Правильная миграция
@Test func asyncTest() async { // 1. Объявляем async тест
let result = await withCheckedContinuation { continuation in
DispatchQueue.main.async {
continuation.resume(returning: 42) // 2. Возвращаем результат в контекст Task
}
}

#expect(result == 42) // 3. Проверяем УЖЕ внутри Task-контекста
}


Ключевые моменты:

  • Тест должен быть async - это автоматически создаёт контекст Task.

  • Все асинхронные операции, которые не поддерживают async/await оборачиваются в withCheckedContinuation.

  • #expect вызывается после получения результата, но внутри области теста.


Проблемы с общим состоянием:

Параллельное выполнение тестов могут вызвать гонки данных.

Пример:

actor Counter {
var count = 0

func increment() {
count += 1
}
}

@Test func testA() async {
await Counter.shared.increment()
#expect(await Counter.shared.count == 1) // Может упасть, если testB запустится раньше!
}

@Test func testB() async {
await Counter.shared.increment()
}


Решение:

Использование @Suite(.serialized) для последовательного выполнения:

@Suite(.serialized)
struct CriticalTests {
@Test func testA() {
}

@Test func testB() {
}
}


Наследование тестов не работает:

В XCTest можно было наследовать XCTestCase и автоматически получать все тесты родителя. В Swift Testing так не работает - тесты нужно дублировать.

Что делать:

  • Для простых сценариев использовать параметризованные тесты.

  • Для сложных, пока оставьте XCTest и мигрируйте постепенно.

Вывод:


На Swift Testing стоит переходить если у вас современный проект, который использует Swift Concurrency и вам необходимо параллельное выполнение тестов.

Мой совет: Начинайте с гибридного подхода - подключайте Swift Testing для новых модулей, а старые тесты оставляйте в XCTest.
25.07.2025 15 407