Skip to content
AgileLogics
6 min read

Testing with Swift Testing and XCTest

Prerequisites: Swift Package Manager

A test is code that checks your other code works. Once written, a test runs in a fraction of a second every time you change something, catching mistakes before your users do.

Swift has two testing frameworks:

  • Swift Testing — the modern framework (Swift 6 / Xcode 16 and later), using @Test and #expect.
  • XCTest — the older framework, still widely used in existing projects and required for UI tests.

The code under test

// Sources/Shop/Cart.swift
public struct Cart {
    public private(set) var items: [(name: String, price: Int)] = []

    public init() {}

    public mutating func add(_ name: String, price: Int) {
        precondition(price > 0, "price must be positive")
        items.append((name, price))
    }

    public var total: Int { items.reduce(0) { $0 + $1.price } }
}

Swift Testing

// Tests/ShopTests/CartTests.swift
import Testing
@testable import Shop

@Test func startsEmpty() {
    let cart = Cart()
    #expect(cart.total == 0)
    #expect(cart.items.isEmpty)
}

@Test func totalAddsPrices() {
    var cart = Cart()
    cart.add("book", price: 12)
    cart.add("pen", price: 3)
    #expect(cart.total == 15)
}

@Test(arguments: [1, 5, 100])
func singleItemTotal(price: Int) {
    var cart = Cart()
    cart.add("item", price: price)
    #expect(cart.total == price)
}
$ swift test
...
✔ Test startsEmpty() passed after 0.001 seconds.
✔ Test totalAddsPrices() passed after 0.001 seconds.
✔ Test singleItemTotal(price:) with 3 test cases passed after 0.001 seconds.
✔ Test run with 3 tests in 0 suites passed after 0.002 seconds.

(Output abbreviated; the exact wording varies a little between Swift versions.)

  • @Test marks a test function.
  • #expect(...) checks a condition. When it fails, the report shows the actual values on each side, such as cart.total → 12 == 15.
  • @Test(arguments:) runs the same test with several inputs.
  • @testable import Shop lets tests use internal members, not just public ones.

The same test in XCTest

import XCTest
@testable import Shop

final class CartTests: XCTestCase {
    func testTotalAddsPrices() {
        var cart = Cart()
        cart.add("book", price: 12)
        cart.add("pen", price: 3)
        XCTAssertEqual(cart.total, 15)
    }
}

Test methods start with test, and assertions are functions such as XCTAssertEqual, XCTAssertTrue, and XCTAssertNil.

In Xcode, press ⌘U to run all tests, or click the diamond next to a single test.

What to test

Test your logic — calculations, validation, parsing, state changes — including edge cases: empty input, zero, the limits of a range, and error cases. Keep that logic out of your views, so it's easy to test.

Common mistakes

  • Testing private implementation details instead of behavior. Test what goes in and what comes out.
  • Tests that depend on the network, the clock, or each other. Make each test self-contained.