XCTest
Swift testing: XCTest, Swift Testing (Swift 6+), expectations, async tests, performance tests, UI tests.
Swift — testing
EXAMPLE
// ===== XCTest (the classic) =====
import XCTest
final class MathTests: XCTestCase {
func testAdd() {
XCTAssertEqual(1 + 1, 2)
}
func testThrows() {
XCTAssertThrowsError(try Divide(10, 0))
}
func testEventually() async throws {
let value = try await fetchValue()
XCTAssertEqual(value, 42)
}
}
// In Xcode: Cmd+U to run.
// ===== Swift Testing (the modern framework, Swift 6+) =====
import Testing
@Test func add() {
#expect(1 + 1 == 2)
}
@Test(arguments: [1, 2, 3])
func double(_ n: Int) {
#expect(n * 2 == n + n)
}
@Test func userCreation() async throws {
let user = try await User.create(name: "Alex")
#expect(user.id != UUID())
#expect(user.name == "Alex")
}
@Suite("Pricing")
struct PricingTests {
@Test func emptyCartIsZero() {
let cart = Cart()
#expect(cart.total == 0)
}
}
// ===== Mocks + dependency injection =====
protocol UserRepo { func find(_ id: Int) -> User? }
struct FakeRepo: UserRepo {
var users: [Int: User]
func find(_ id: Int) -> User? { users[id] }
}
@Test func service() {
let repo = FakeRepo(users: [1: User(id: 1, name: "Alex")])
let svc = UserService(repo: repo)
#expect(svc.greet(id: 1) == "Hi Alex")
}
// ===== XCTest expectations (legacy async) =====
func testCallback() {
let exp = expectation(description: "completion")
fetchUser { result in
XCTAssertNotNil(result)
exp.fulfill()
}
waitForExpectations(timeout: 5)
}
// Swift Testing's async support replaces most of this.
// ===== Performance =====
final class PerfTests: XCTestCase {
func testParseBigJSON() {
measure {
_ = try? JSONDecoder().decode([User].self, from: bigJSON)
}
}
}
// ===== UI tests (XCUITest) =====
final class UIFlowTests: XCTestCase {
func testTapButton() {
let app = XCUIApplication()
app.launch()
app.buttons["Sign in"].tap()
XCTAssertTrue(app.textFields["Email"].waitForExistence(timeout: 3))
}
}
// ===== Code coverage =====
// Xcode -> Edit Scheme -> Test -> Options -> Gather coverage data.
// ===== Setup + teardown =====
final class APITests: XCTestCase {
var sut: API!
override func setUp() async throws {
sut = try await API.test()
}
override func tearDown() async throws {
await sut.shutdown()
}
}
// Swift Testing alternative:
@Suite struct APITests {
let api: API
init() async throws { api = try await API.test() }
}
// ===== Patterns =====
// - Swift Testing for new code; XCTest for legacy + UI tests
// - Inject dependencies via protocols; use fakes
// - Use #expect with descriptive messages
// - Parameterise tests with @Test(arguments: ...)
// ===== Pitfalls =====
// - Tests that hit the network -> flaky
// - Singletons in production code -> hard to test
// - Long test runtimes -> shard via test plans
// - XCUITest in CI without simulators warmed up -> slow
Why it matters
XCTest is the workhorse; Swift Testing (Swift 6+) is the modern alternative with #expect and @Test. Inject via protocols, fake in tests, parameterise, and add coverage in the scheme. UI tests via XCUITest; performance via measure. The discipline turns Swift code into a refactor-friendly codebase.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import XCTest
final class CalcTests: XCTestCase {
func testAdd() {
XCTAssertEqual(2 + 3, 5)
}
}
Try it Yourself »
Discussion
Loading…