Testing in Swift

XCTest, XCTestCase with setUp/tearDown, assertions, and testing throwing and async code.

XCTest — Swift's standard testing framework

XCTest is Apple's built-in testing framework, bundled with Xcode and the Swift toolchain — no separate dependency needed to get started, similar in spirit to how cargo test ships with Rust. A test target in an Xcode project (or a swift test target in a Swift package) compiles test files separately from the app/library itself and runs every test method automatically.

Bash
swift test              # runs all tests in a Swift package
# or, in Xcode: Cmd+U, or Product > Test

XCTestCase, setUp, and tearDown

Tests are grouped into subclasses of XCTestCase; each method whose name starts with test is treated as one individual test. setUp() runs before every test method in the class, and tearDown() runs after every one — the standard place to establish and then clean up fresh state per test, rather than letting state leak between tests:

Swift
import XCTest
@testable import MyApp

final class CalculatorTests: XCTestCase {
    var calculator: Calculator!

    override func setUp() {
        super.setUp()
        calculator = Calculator()
    }

    override func tearDown() {
        calculator = nil
        super.tearDown()
    }

    func testAddingTwoNumbers() {
        XCTAssertEqual(calculator.add(2, 3), 5)
    }

    func testAddingNegativeNumbers() {
        XCTAssertEqual(calculator.add(-2, -3), -5)
    }
}

@testable import MyApp imports the app/library module with access to its internal declarations, not just public ones — letting tests reach code that isn't meant to be part of the module's external API, while still respecting private/fileprivate.

Assertions

Assertion Checks
XCTAssertEqual(a, b) a == b
XCTAssertTrue(x) / XCTAssertFalse(x) A boolean condition
XCTAssertNil(x) / XCTAssertNotNil(x) An optional is (or isn't) nil
XCTAssertThrowsError(expression) The expression throws an error
XCTAssertNoThrow(expression) The expression does not throw
XCTFail("message") Unconditionally fails the test, with a message

A complete test case, including error handling

Swift
enum CalculatorError: Error, Equatable {
    case divisionByZero
}

struct Calculator {
    func add(_ a: Int, _ b: Int) -> Int {
        a + b
    }

    func divide(_ a: Int, by b: Int) throws -> Double {
        guard b != 0 else {
            throw CalculatorError.divisionByZero
        }
        return Double(a) / Double(b)
    }
}
Swift
import XCTest
@testable import MyApp

final class CalculatorTests: XCTestCase {
    var calculator: Calculator!

    override func setUp() {
        super.setUp()
        calculator = Calculator()
    }

    func testAddingTwoNumbers() {
        XCTAssertEqual(calculator.add(2, 3), 5)
    }

    func testDividingReturnsCorrectQuotient() throws {
        let result = try calculator.divide(10, by: 4)
        XCTAssertEqual(result, 2.5)
    }

    func testDividingByZeroThrows() {
        XCTAssertThrowsError(try calculator.divide(10, by: 0)) { error in
            XCTAssertEqual(error as? CalculatorError, CalculatorError.divisionByZero)
        }
    }
}

testDividingReturnsCorrectQuotient is itself marked throws — a test method is allowed to throw, and XCTest treats an uncaught error the same as a failed assertion, so try can be used directly without wrapping every call in its own do/catch. XCTAssertThrowsError takes a trailing closure that receives the actual thrown error, letting the test verify not just that something was thrown, but which error specifically.

Testing async code

XCTest supports async test methods directly — mark the test function async (and throws, if needed) and use await inside it exactly like any other asynchronous Swift code:

Swift
func testFetchUserNameReturnsExpectedName() async throws {
    let name = try await fetchUserName(id: 1)
    XCTAssertEqual(name, "Ali")
}

No special wrapper or completion-handler juggling is required — the test simply suspends at each await the same way any other async function would, and XCTest waits for it to finish before reporting a pass or fail.

Common mistakes

  • Overriding setUp()/tearDown() but forgetting to call super.setUp()/super.tearDown() — XCTest relies on the superclass implementation for some of its own bookkeeping, and skipping it can produce confusing, hard-to-diagnose test failures.
  • Sharing mutable state through a static property or global variable instead of resetting it in setUp(), which makes tests pass or fail depending on execution order rather than on their own merits.
  • Testing private implementation details reached only through @testable import instead of the type's actual public behavior — this makes tests brittle, breaking on harmless internal refactors that don't change any observable behavior at all.
  • Writing an assertion with the arguments in the wrong order out of habit from another language's testing framework — XCTAssertEqual(actual, expected) is the Swift convention (expected second), and getting it backwards doesn't break anything, but makes failure messages read confusingly.

Interview questions

Q: What's the purpose of setUp() and tearDown() in an XCTestCase? setUp() runs before every individual test method, and is the standard place to construct fresh objects a test needs (like a new Calculator instance) so no state leaks in from a previous test. tearDown() runs after every test method, used for cleanup like releasing resources — together they guarantee each test starts from the same known, isolated state.

Q: How do you test that a function throws a specific error in XCTest? Wrap the call in XCTAssertThrowsError(try someThrowingCall()), optionally with a trailing closure that receives the thrown error so you can assert on which specific error it was (e.g., XCTAssertEqual(error as? MyError, MyError.someCase)), rather than just confirming that something was thrown.

Q: What does @testable import give you access to that you wouldn't otherwise have? It imports a module with visibility into its internal declarations, not just the public ones a normal import would expose — letting a test target verify internal (but not private/fileprivate) types and functions that aren't part of the module's intentionally exposed public API.