XCTest Framework
XCTest is Apple's built-in testing framework. It comes with Xcode and lets you write unit tests, performance tests, and UI tests. Tests run automatically and give you clear pass/fail results. Good tests catch bugs before your users do.
import XCTest
@testable import MyApp
final class CalculatorTests: XCTestCase {
func testAddition() {
let calc = Calculator()
XCTAssertEqual(calc.add(2, 3), 5)
}
}
Try it Yourself ->
Unit Tests with XCTestCase
Each test is a method that starts with test. Create an instance of your class, call methods, and verify results. XCTest reports which tests pass and which fail.
final class MathTests: XCTestCase {
func testMultiply() {
let result = Math.multiply(4, 5)
XCTAssertEqual(result, 20)
}
func testDivide() {
let result = Math.divide(10, by: 2)
XCTAssertEqual(result, 5)
}
func testDivideByZero() {
XCTAssertThrowsError(Math.divide(10, by: 0)) { error in
XCTAssertEqual(error as? MathError, .divisionByZero)
}
}
}
Try it Yourself ->
XCTAssert Functions
XCTest gives you many assertion functions. Use the right one for each check. More specific assertions give better failure messages, making it easier to find and fix problems.
func testExamples() {
// Equality
XCTAssertEqual(a, b, "Values should be equal")
// Boolean
XCTAssertTrue(isValid)
XCTAssertFalse(isEmpty)
// Nil checks
XCTAssertNotNil(result)
XCTAssertNil(error)
// Identical
XCTAssertTrue(obj1 === obj2)
// Throwing
XCTAssertNoThrow(try riskyOperation())
}
Try it Yourself ->
setUp and tearDown
Use setUp to create shared resources before each test. Use tearDown to clean up after. This keeps tests isolated — one test's state won't leak into another.
final class DatabaseTests: XCTestCase {
var database: Database!
override func setUp() {
super.setUp()
database = Database.inMemory()
}
override func tearDown() {
database = nil
super.tearDown()
}
func testInsert() {
database.insert(name: "Alice")
XCTAssertEqual(database.count, 1)
}
func testDelete() {
database.insert(name: "Bob")
database.delete(name: "Bob")
XCTAssertEqual(database.count, 0)
}
}
Try it Yourself ->
UI Tests with XCUITest
UI tests simulate real user interaction — tapping buttons, typing text, scrolling. They verify that your interface works end to end, not just that individual functions return correct values.
import XCTest
final class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
continueAfterFailure = false
app.launch()
}
func testLoginFlow() {
let emailField = app.textFields["email"]
let passwordField = app.textFields["password"]
let loginButton = app.buttons["Login"]
emailField.tap()
emailField.typeText("user@example.com")
passwordField.tap()
passwordField.typeText("secret123")
loginButton.tap()
XCTAssertTrue(app.staticTexts["Welcome!"].waitForExistence(timeout: 5))
}
}
Try it Yourself ->
Testing Async Code
Test async functions with XCTExpectFailure or use async test methods. XCTest supports async/await so your tests can call async functions directly with await.
final class NetworkTests: XCTestCase {
func testFetchUser() async throws {
let user = try await UserService.fetchUser(id: 1)
XCTAssertEqual(user.name, "Alice")
}
func testFetchFailsWithBadID() async {
do {
_ = try await UserService.fetchUser(id: -1)
XCTFail("Should have thrown an error")
} catch {
XCTAssertTrue(error is NetworkError)
}
}
}
Try it Yourself ->
Code Coverage
Xcode tracks which lines of your code tests actually run. High coverage means fewer untested paths. Enable it in your scheme settings and check the report after running tests.
// Run tests with coverage in Xcode:
// 1. Edit Scheme -> Test -> Options -> Code Coverage
// 2. Enable "Code Coverage" for your target
// 3. Run tests (Cmd+U)
// 4. Check Report Navigator for coverage details
//
// Aim for 70-80%+ coverage on critical paths
// Don't chase 100% — test what matters
Try it Yourself ->