XCTest vs Swift Testing: When Unit Tests Grow Up 🚀🧪
Published:
Listen up, fellow code warriors! Remember the days when writing unit tests felt like wrestling an angry octopus? Well, Apple’s been working some magic, and the new Swift Testing framework is here to save our sanity (and our carpal tunnels).
The Old Guard: XCTest - Our Loyal Testing Companion
Basic Assertions
// XCTest
func testSimpleComparison() {
let value = 42
XCTAssertEqual(value, 42, "The answer should be 42")
XCTAssertNotEqual(value, 0, "Value should not be zero")
}
// Swift Testing
func testSimpleComparison() {
let value = 42
#expect(value == 42)
#expect(value != 0)
}
Throwing Function Tests
// XCTest
func testThrowingFunction() {
do {
try performRiskyOperation()
XCTFail("Should have thrown an error")
} catch {
XCTAssertTrue(error is CustomError)
}
}
// Swift Testing
func testThrowingFunction() throws {
#expect(throws: CustomError.self) {
try performRiskyOperation()
}
}
Async Testing
// XCTest
func testAsyncOperation() async {
let expectation = XCTestExpectation(description: "Fetch user")
Task {
do {
let user = try await userService.fetchUser()
XCTAssertNotNil(user)
expectation.fulfill()
} catch {
XCTFail("Fetch should succeed")
}
}
await fulfillment(of: [expectation], timeout: 5.0)
}
// Swift Testing
func testAsyncOperation() async throws {
let user = try await userService.fetchUser()
#expect(user != nil)
}
Parameterized Testing
// XCTest
func testEmailValidation() {
let validator = EmailValidator()
let testCases = [
("valid@email.com", true),
("invalid-email", false),
("another.valid@test.co.uk", true)
]
testCases.forEach { email, expectedResult in
XCTAssertEqual(
validator.isValid(email),
expectedResult,
"Failed for email: \(email)"
)
}
}
// Swift Testing
func testEmailValidation() {
let validator = EmailValidator()
#expect(validator.isValid("valid@email.com") == true)
#expect(validator.isValid("invalid-email") == false)
#expect(validator.isValid("another.valid@test.co.uk") == true)
}
// Swift Testing with Variants (More Powerful!)
@Test(arguments: [
("valid@email.com", true),
("invalid-email", false),
("another.valid@test.co.uk", true)
])
func testEmailValidation(email: String, isValid: Bool) {
let validator = EmailValidator()
#expect(validator.isValid(email) == isValid)
}
Error Handling and Expectations
// XCTest
func testErrorHandling() {
let service = NetworkService()
XCTAssertThrowsError(try service.criticalOperation()) { error in
XCTAssertTrue(error is NetworkError)
guard let networkError = error as? NetworkError else {
XCTFail("Wrong error type")
return
}
XCTAssertEqual(networkError.code, 404)
}
}
// Swift Testing
func testErrorHandling() throws {
let service = NetworkService()
#expect(throws: NetworkError.self) {
try service.criticalOperation()
} validate: { error in
#expect(error.code == 404)
}
}
Skipping and Conditional Tests
// XCTest
func testOnlyOnMac() {
guard #available(macOS 14.0, *) else {
XCTSkip("Only runs on macOS 14+")
return
}
// Test implementation
}
// Swift Testing
@Test(.disabled("Work in progress"))
func experimentalFeature() {
// Future implementation
}
@Test(.skippedIf(ProcessInfo.processInfo.operatingSystemVersion.majorVersion < 14))
func testOnlyOnNewMacOS() {
// macOS 14+ specific test
}
The Verdict: Evolution in Action
Swift Testing isn’t just a new framework—it’s a paradigm shift. With more concise syntax, powerful macros, and intuitive test writing, it’s like going from a bicycle to a Tesla of testing frameworks.
Pro Tips:
- Gradual migration is key
- Embrace the
#expect
macro - Experiment with new testing patterns
- Don’t fear change—welcome it!
Happy Testing, Swift Ninja! 🥷🏼👩‍💻👨‍💻