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
- • Write tests in an XCTestCase subclass
- • Verify results with XCTAssertEqual and friends
- • Prepare and clean state with setUp / tearDown
- • Test async functions with await
- • Mock dependencies by coding to a protocol
- • Meet the new Swift Testing framework (@Test, #expect)
📊 Common Assertions
| Assertion | Passes 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
- 💡 One behaviour per test. A focused test points straight at the broken thing when it fails.
- 💡 Inject dependencies via protocols so you can mock the network, clock, or database.
- 💡 Keep tests fast and offline. Real network calls make tests flaky and slow.
- 💡 Name tests for behaviour, e.g. testDivideByZeroReturnsNil.
Common Errors (and the fix)
- Test never runs — its method name doesn't start with test.
- Tests interfere with each other — reset shared state in setUp/tearDown.
- "async" call not awaited — mark the test method async and use await.
- Flaky tests — you're hitting the real network; inject a mock instead.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Test class | class T: XCTestCase | subclass |
| Test method | func testThing() | test* prefix |
| Equality | XCTAssertEqual(a, b) | a == b |
| Setup | override func setUp() | before each |
| Async | func testX() async | use 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!
- ✅ Tests live in an XCTestCase subclass; methods start with test
- ✅ Verify with XCTAssertEqual, XCTAssertTrue, XCTAssertNil
- ✅ setUp/tearDown keep tests isolated
- ✅ Mark a test async to test async code; mock via protocols
- ✅ Swift Testing's @Test and #expect are the modern alternative
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
- Previous: SwiftData Basics
- Next: Swift Package Manager — Package.swift, dependencies, targets and adding packages
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.