Unit Testing with XCTest

Reviewed & published by Brayan K

Tests prove your code works and keep it working as you change it. You'll write XCTestCase tests, assert with XCTAssertEqual, use setUp/tearDown, test async code, mock with protocols, and meet the new Swift Testing framework.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

📊 Common Assertions

AssertionPasses when
XCTAssertEqual(a, b)a equals b
XCTAssertTrue(x)x is true
XCTAssertFalse(x)x is false
XCTAssertNil(x)x is nil
XCTUnwrap(x)x is non-nil (returns it)

1️⃣ Your First Test

Subclass XCTestCase, write methods that start with test, and assert the result. Xcode finds and runs them automatically.

import XCTest

// Code under test:
func add(_ a: Int, _ b: Int) -> Int { a + b }

// A test class subclasses XCTestCase. Methods starting with "test" run.
final class MathTests: XCTestCase {

    func testAddPositiveNumbers() {
        let result = add(2, 3)
        XCTAssertEqual(result, 5)   // fails if result != 5
    }

    func testAddIsCommutative() {
        XCTAssertEqual(add(1, 4), add(4, 1))
    }
}

// In Xcode: press Cmd+U to run the test suite.

2️⃣ setUp and tearDown

setUp() runs before each test to build fresh state; tearDown() runs after to clean it up. This keeps every test isolated and repeatable.

import XCTest

final class CartTests: XCTestCase {
    var cart: [String]!

    // setUp runs BEFORE each test — fresh state every time.
    override func setUp() {
        super.setUp()
        cart = []
    }

    // tearDown runs AFTER each test — clean up.
    override func tearDown() {
        cart = nil
        super.tearDown()
    }

    func testStartsEmpty() {
        XCTAssertTrue(cart.isEmpty)
    }

    func testAddItem() {
        cart.append("Book")
        XCTAssertEqual(cart.count, 1)
    }
}

Your turn. Fill in the base class, the method prefix, and the assertion.

// 🎯 YOUR TURN — replace each ___ then run in Xcode.
import XCTest

func double(_ n: Int) -> Int { n * 2 }

final class DoubleTests: ___ {          // 👉 the base class for tests
    func ___Double() {                  // 👉 the prefix every test method needs
        XCTAssert___(double(5), 10)     // 👉 the assertion that checks equality
    }
}

// ✅ Once filled in, the test passes because double(5) == 10.

3️⃣ Async Tests and Mocking

Mark a test method async to await async code directly. Coding to a protocol lets you swap in a fast, offline mock during tests.

import XCTest

// Code to a PROTOCOL so tests can inject a fake.
protocol Fetcher { func fetch() async -> Int }

// Real implementation (would hit the network).
struct LiveFetcher: Fetcher { func fetch() async -> Int { 42 } }

// A MOCK conforming type returns a fixed value, fast and offline.
struct MockFetcher: Fetcher { func fetch() async -> Int { 7 } }

final class FetcherTests: XCTestCase {
    // Async test: mark the method async and await directly.
    func testMockReturnsSeven() async {
        let sut: Fetcher = MockFetcher()
        let value = await sut.fetch()
        XCTAssertEqual(value, 7)
    }
}

Now you try. Fill in the keywords that make a test async-aware.

// 🎯 YOUR TURN — test async code with a mock.
import XCTest

protocol Clock { func now() async -> Int }
struct FakeClock: Clock { func now() async -> Int { 100 } }

final class ClockTests: XCTestCase {
    func testNow() ___ {                 // 👉 keyword so the test can await
        let c: Clock = FakeClock()
        let t = ___ c.now()              // 👉 keyword that waits for the result
        XCTAssertEqual(t, 100)
    }
}

// ✅ The test passes because FakeClock returns 100.

4️⃣ The New Swift Testing Framework

Apple's newer Swift Testing framework uses the @Test macro and #expect instead of XCTest's class and assertions. It reads more naturally and coexists with XCTest.

import Testing

// Swift Testing (newer) uses the @Test macro and #expect instead of XCTest.
func add(_ a: Int, _ b: Int) -> Int { a + b }

@Test func addsNumbers() {
    #expect(add(2, 3) == 5)   // #expect replaces XCTAssert
}

@Test func handlesZero() async {
    #expect(add(0, 0) == 0)
}

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Test classclass T: XCTestCasesubclass
Test methodfunc testThing()test* prefix
EqualityXCTAssertEqual(a, b)a == b
Setupoverride func setUp()before each
Asyncfunc testX() asyncuse await
Swift Testing@Test func x() { #expect(…) }newer API

Mini-Challenge: Test a Calculator

Write a small test class with setUp, an equality test, and a nil test for a divide function.

// 🎯 MINI-CHALLENGE: Test a small calculator
// Given:  struct Calc { func divide(_ a: Int, _ b: Int) -> Int? { b == 0 ? nil : a / b } }
// Write a test class with:
//   - setUp() that creates a fresh Calc.
//   - testNormalDivision() asserting divide(10, 2) == 5 with XCTAssertEqual.
//   - testDivideByZeroReturnsNil() asserting divide(1, 0) is nil with XCTAssertNil.
//
// ✅ Both tests should pass.

// your code here

🎉 Lesson Complete!

Practice quiz

Which base class do XCTest test classes inherit from?

  • TestCase
  • XCTestCase
  • UITest
  • XCTAssert

Answer: XCTestCase. Test classes subclass XCTestCase in the XCTest framework.

Which assertion checks two values are equal?

  • XCTAssertTrue
  • XCTAssertNil
  • XCTAssertEqual
  • XCTFail

Answer: XCTAssertEqual. XCTAssertEqual(a, b) fails the test when a and b differ.

By convention, an XCTest test method name must start with...

  • check
  • test
  • spec
  • it

Answer: test. Methods beginning with 'test' are discovered and run automatically.

When does setUp() run?

  • Before each test method
  • Once for the whole class
  • After all tests
  • Only on failure

Answer: Before each test method. setUp() runs before every test method to prepare fresh state.

When does tearDown() run?

  • Before each test
  • Never
  • Only once at launch
  • After each test method

Answer: After each test method. tearDown() runs after each test to clean up.

How do you test an async function in XCTest?

  • You cannot
  • Mark the test method async and use await
  • Use a Timer
  • Use DispatchQueue.main.sync

Answer: Mark the test method async and use await. Modern XCTest lets a test method be async so it can await directly.

Why depend on a protocol to enable mocking?

  • It runs faster
  • It avoids imports
  • You can substitute a fake conforming type in tests
  • It removes assertions

Answer: You can substitute a fake conforming type in tests. Coding to a protocol lets you inject a mock implementation during tests.

Which assertion fails when a condition is false?

  • XCTAssertTrue
  • XCTAssertFalse
  • XCTAssertNil
  • XCTUnwrap

Answer: XCTAssertTrue. XCTAssertTrue fails if the expression evaluates to false.

What is Apple's newer testing framework that uses #expect and @Test?

  • Quick
  • Nimble
  • SnapshotTesting
  • Swift Testing

Answer: Swift Testing. Swift Testing is Apple's modern framework using the @Test macro and #expect.

A good unit test should be...

  • Slow and order-dependent
  • Fast, isolated, and repeatable
  • Reliant on the network
  • Random in outcome

Answer: Fast, isolated, and repeatable. Unit tests should be fast, isolated, and deterministic.

Continue this course

Frequently asked questions

How do I write a unit test in XCTest?

Create a class that subclasses XCTestCase, then add methods whose names begin with the word test. Inside each, call your code under test and verify the outcome with assertions like XCTAssertEqual. Xcode discovers these methods automatically and runs them when you press Cmd+U. Each test method is independent, so order should not matter.

What are setUp and tearDown for?

setUp() runs before every test method, giving each test a fresh starting state, such as a newly created object. tearDown() runs after every test method to release resources or reset shared state. Using them keeps tests isolated: one test cannot leak state into another, which is essential for reliable, repeatable results.

How do I test asynchronous code?

Modern XCTest lets a test method itself be marked async. Inside it you simply await the asynchronous call and then assert on the result, just like normal code. This is far cleaner than the older XCTestExpectation and waitForExpectations pattern, though that approach still exists for callback-based APIs that do not use async/await.

Why use protocols for mocking?

If your code depends on a protocol rather than a concrete type, you can inject a fake implementation during tests. For example, a networking protocol can have a live version that hits the server and a mock version that returns fixed data instantly and offline. This makes tests fast, deterministic, and independent of external systems you do not control.

What is the new Swift Testing framework?

Swift Testing is Apple's modern testing framework that uses the @Test macro to mark test functions and the #expect macro for assertions, replacing the XCTAssert family. It offers clearer failure messages, parameterized tests, and a more Swift-native feel. XCTest remains fully supported, and the two can coexist in the same project while teams migrate.