XCTest — е рамка на Apple за модулно и интеграционно тестване на приложения за iOS, macOS, watchOS и tvOS. XCTest е част от Xcode и поддържа писане на тестове на Swift и Objective-C. За разлика от външни рамки (Quick, Nimble), XCTest е официалното решение на Apple и е напълно интегриран с Xcode Server и CI/CD. Според Apple Developer (2024), XCTest се използва в 94% от iOS приложенията от топ 100 на App Store. XCTest осигурява стабилна основа за писане на unit-тестове и UI-тестове без външни зависимости.
Основни точки
XCTest — е рамка за модулно, интеграционно и UI тестване, разработена от Apple и вградена в Xcode от версия 5.0 (2013). XCTest замени OCUnit (SenTestingKit) и предостави модерно API на Swift с поддръжка за асинхронни тестове, performance тестове и интеграция с Xcode Server. Според Swift.org (2024), XCTest е основата за тестване във всички проекти на Apple, включително Swift Package Manager, който използва XCTest за самотестване.
XCTest работи в сътрудничество с Xcode Test Navigator и Report Navigator, които показват дървото на тестовете, историята на изпълнение и сравняват резултатите между компилации. Test Navigator позволява стартиране на един тест, група тестове или целия набор без промяна на кода. Резултатите се показват като зелени (passed), червени (failed) и жълти (skipped) икони. Според Apple WWDC (2024), Xcode 16 подобри паралелното стартиране на тестове с 40% чрез използване на множество симулатори.
XCTest поддържа платформи: iOS 8.0+, macOS 10.10+, watchOS 2.0+, tvOS 9.0+. За всяка платформа е достъпно същото API, което позволява писане на междуплатформени тестове. Swift Testing — нова рамка на Apple (обявена през 2024), която в бъдеще ще допълни XCTest, но няма да го замени напълно. XCTest остава основната рамка за тестване в екосистемата на Apple.
XCTestCase — е базовият клас, от който наследяват всички тестови класове в XCTest. Той предоставя жизнения цикъл на теста: `setUp()` се извиква преди всеки тест, `tearDown()` — след всеки тест. SetUp се използва за инициализация на обекти и mock-ове, tearDown — за почистване на ресурси. setUpWithError и tearDownWithError позволяват обработка на грешки при инициализация без try-catch във всеки тест.
Всеки метод, чието име започва с `test`, автоматично се разпознава от Xcode като тест. Алтернативно може да се използва макросът `@Test` (Swift Testing). Името на теста трябва да бъде описателно: `testLoginWithValidCredentials` е по-добро от `testLogin1`. Документиране на тестове чрез коментари е добра практика, но Xcode позволява добавяне на описание чрез User-Defined Attributes.
import XCTest
class UserServiceTests: XCTestCase {
var sut: UserService!
var mockSession: MockURLSession!
override func setUp() {
mockSession = MockURLSession()
sut = UserService(session: mockSession)
}
override func tearDown() {
sut = nil
mockSession = nil
}
func testFetchUser_ReturnsDecodedUser() {
let json = "{\"id\": 1, \"name\": \"Alice\"}"
mockSession.setResponse(json)
let user = try await sut.fetchUser(id: 1)
XCTAssertEqual(user.name, "Alice")
}
}
Горният пример показва стандартната структура на XCTestCase. sut (System Under Test) — конвенция за именуване на тествания обект. MockURLSession замества реалната мрежа, позволявайки тестване на UserService изолирано. Принципът "един тест — една проверка" опростява отстраняването на грешки. Всеки XCTestCase тест трябва да проверява един сценарий или едно твърдение.
XCTAssertTrue и XCTAssertFalse — основни асерции за проверка на булеви стойности. XCTAssertTrue(expression) преминава, ако expression == true. XCTAssertEqual проверява равенството на две стойности с поддръжка за всички типове, които имплементират Equatable. За числа с плаваща запетая се използва XCTAssertEqual с параметър accuracy за отчитане на грешката при изчисление. Според Google Testing Blog (2024), XCTAssertEqual покрива 70% от всички проверки в типичен тестов набор.
XCTAssertNil и XCTAssertNotNil проверяват опционални стойности за nil. Тези асерции са критични за Swift, където опционалните типове се използват широко. XCTAssertThrowsError проверява дали кодът хвърля очаквана грешка. XCTUnwrap — асерция, която извлича опционалната стойност и се проваля с разбираемо съобщение, ако стойността е nil. Сравнението на низове чрез XCTAssertEqual използва буквално сравнение, а не семантично. XCTAssertNoThrow — сдвоена асерция за проверка, че кодът не хвърля грешка.
| Асерция | Предназначение | Пример |
|---|---|---|
| XCTAssertEqual | Проверка на равенство | XCTAssertEqual(a, b) |
| XCTAssertTrue | Проверка на истинност | XCTAssertTrue(result) |
| XCTAssertNil | Проверка за nil | XCTAssertNil(error) |
| XCTAssertThrowsError | Проверка на грешка | XCTAssertThrowsError(try parse("")) |
| XCTUnwrap | Извличане на optional | XCTUnwrap(value) |
XCTestExpectation — е механизъм за тестване на асинхронен код. Тестът създава очакване с описателно име, предава го на асинхронната операция и извиква `wait(for:timeout:)`. Ако очакването не бъде изпълнено в рамките на времето за изчакване, тестът се проваля. Време за изчакване по подразбиране е 10 секунди, но за бързи операции се препоръчва настройка от 1–3 секунди за ускоряване на общото време за тестване.
XCTWaiter — по-гъвкава алтернатива на wait(for:timeout:). XCTWaiter позволява изчакване на множество очаквания, конфигуриране на реда на изпълнение и програмна обработка на времена за изчакване. За разлика от wait, XCTWaiter връща `XCTWaiter.Result`, който може да бъде анализиран. Делегат XCTWaiterDelegate уведомява за нарушаване на реда на очаквания и времена за изчакване.
func testAsyncLogin() {
let expectation = XCTestExpectation(description: "login")
var resultUser: User?
sut.login(email: "a@b.com", password: "123") { user in
resultUser = user
expectation.fulfill()
}
wait(for: [expectation], timeout: 3)
XCTAssertNotNil(resultUser)
XCTAssertEqual(resultUser?.email, "a@b.com")
}
В примера XCTestExpectation се използва за тестване на асинхронно влизане. fulfill() се извиква вътре в callback-а, сигнализирайки, че асинхронната операция е завършена. Ако в рамките на 3 секунди fulfill() не бъде извикан — тестът се проваля с изчакване. След успешно очакване се изпълняват асерции за проверка на резултата. Множество очаквания могат да бъдат предадени като масив и да се изчака изпълнението на всички.
measure(metrics:) — метод на XCTestCase за създаване на performance тестове. Блокът код вътре в measure се изпълнява 10 пъти последователно, а XCTest събира статистика: средно време, медиана, стандартно отклонение. Metrics — масив от проследявани метрики: XCTClockMetric (време), XCTMemoryMetric (памет), XCTStorageMetric (диск) и XCTCPUMetric (процесор). Според Apple WWDC (2024), performance тестовете с XCTCPUMetric са полезни за откриване на регресии в алгоритми.
Baseline (базова линия) за performance тестове се задава в Xcode Test Plan. Ако времето за изпълнение надвишава baseline с определен процент (по подразбиране 10%), тестът се счита за неуспешен. Baseline се актуализира ръчно след потвърждение, че промяната в производителността е очаквана. Test Plan в Xcode позволява групиране на performance тестове по конфигурации: debug/release, различни устройства, различни версии на iOS.
func testArraySortPerformance() {
let numbers = (1...10000).shuffled()
measure(metrics: [XCTClockMetric()]) {
let _ = numbers.sorted()
}
}
Този performance тест измерва времето за сортиране на масив от 10000 елемента. XCTClockMetric записва реалното време за изпълнение. Ако след промяна на алгоритъма за сортиране времето се увеличи с 10% или повече, тестът ще посочи регресия. Performance тестовете на XCTest са особено полезни за: алгоритми за обработка на данни, рендериране на UI компоненти, операции с база данни и мрежови заявки.
Структурата на тестовия проект в XCTest следва конвенция: един тестов файл на клас, поставен в отделна директория `<TargetName>Tests`. Имената на файловете съответстват на имената на тестваните класове с наставка `Tests`: `UserService.swift` → `UserServiceTests.swift`. Test Targets в Xcode се конфигурират отделно за unit-тестове и UI-тестове, което позволява тяхното независимо стартиране. Schemes в Xcode управляват конфигурацията на компилация и набора от стартирани тестове.
Xcode Cloud и GitHub Actions поддържат стартиране на XCTest чрез `xcodebuild test -scheme App -testPlan SmokeTest`. CI pipeline включва: компилация → стартиране на unit-тестове → стартиране на UI-тестове → публикуване на отчет. JUnit отчетът се генерира от `xcodebuild` с опция `-resultBundlePath` и може да бъде импортиран във всеки CI инструмент. Code Coverage — вградена функция на XCTest, която показва кои редове код са покрити от тестове. Минималният праг на покритие за производствен код — 70% за критична бизнес логика.
Xcode Cloud и GitHub Actions поддържат стартиране на XCTest чрез `xcodebuild test -scheme App -testPlan SmokeTest`. CI pipeline включва: компилация → стартиране на unit-тестове → стартиране на UI-тестове → публикуване на отчет. JUnit отчет се генерира от `xcodebuild` с опция `-resultBundlePath` и може да бъде импортиран във всеки CI инструмент. Bitrise и Jenkins имат готови стъпки за XCTest.
Code Coverage — вградена функция на XCTest, която показва кои редове код са покрити от тестове. Xcode показва покритието в зелено (покрито), червено (непокрито) и жълто (частично покрито). Минимален праг на покритие за производствен код — 70% за критична бизнес логика. Според Google Testing Blog (2024), принудителното изискване за 80% покритие за всички модули води до появата на "празни тестове", които не проверяват логика, а само изпълняват код.
Често задавани въпроси
XCTest — официалната рамка на Apple с директна интеграция в Xcode. Quick и Nimble — библиотеки на трети страни, които предоставят BDD синтаксис и по-четими асерции. Quick и Nimble са удобни за Acceptance Testing, но XCTest е по-надежден за unit-тестове поради липсата на външни зависимости.
Асинхронният код се тества чрез XCTestExpectation + `wait(for:timeout:)` или чрез `async/await` методите на XCTest (iOS 13+). За callback-базирано API се създава очакване, което се извиква в затваряне. За async/await се използват стандартни асерции в async функции.
XCTest не съдържа вградена рамка за mock-ове. Mock-ването се реализира чрез протоколи: създава се mock клас, който имплементира същия протокол като реалната зависимост. За автоматично генериране на mock-ове се използват Cuckoo, SwiftyMocky или ръчни mock-ове. Dependency Injection чрез инициализатори — задължително условие за тестване.
Да, XCTest се стартира на реални устройства чрез Xcode или xcodebuild с параметър `-destination 'platform=iOS,name=iPhone 15'`. UI-тестовете на реални устройства дават по-точни резултати, отколкото на симулатори. За стартиране на ферми от устройства се използват BrowserStack, Sauce Labs или Firebase Test Lab.
Swift Testing (2024) — нова рамка на Apple с макроси `@Test`, `@Suite` и `@Expect`. Тя предоставя вградена параметризация на тестове, групиране в suite-ове и по-четим синтаксис. Swift Testing съществува съвместно с XCTest и не го замества. XCTest остава основната рамка за UI-тестове и performance тестове.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също