С выходом
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.