Swift Testing
Swift Testing Framework (Swift 6+)
Prefer the new @Test / #expect API over XCTest for new code.
import Testing
struct MathTests {
@Test func addition() {
#expect(2 + 2 == 4)
}
@Test("Subtraction is inverse of addition")
func subtraction() {
let result = 10 - 3
#expect(result == 7)
}
@Test func throws() async throws {
#expect(throws: ValidationError.self) {
try validate("")
}
}
}
Parameterized Tests
@Test("Validates email addresses", arguments: [
("user@example.com", true),
("invalid-email", false),
("a@b.c", true),
("@noDomain.com", false),
])
func emailValidation(email: String, isValid: Bool) {
#expect(validateEmail(email) == isValid)
}
Suites and Tags
@Suite("User registration")
struct RegistrationTests {
@Test(.tags(.critical)) func successfulRegistration() async throws { ... }
@Test(.tags(.edge)) func duplicateEmail() async throws { ... }
@Test(.disabled("Flaky on CI — see #1234")) func raceCondition() { ... }
}
extension Tag {
@Tag static var critical: Self
@Tag static var edge: Self
}
Async Tests
@Test func loadsUserAsync() async throws {
let service = UserService(client: MockHTTPClient())
let user = try await service.load(id: UUID())
#expect(user.name == "Alice")
}
XCTest Patterns (legacy / UIKit integration tests)
import XCTest
final class CartTests: XCTestCase {
var sut: Cart!
override func setUp() {
super.setUp()
sut = Cart()
}
override func tearDown() {
sut = nil
super.tearDown()
}
func testAddItem_increasesCount() {
let item = Item(name: "Book", price: 9.99)
sut.add(item)
XCTAssertEqual(sut.items.count, 1)
}
func testAsync() async throws {
let result = try await sut.checkout()
XCTAssertTrue(result.success)
}
}
Protocol-Based Mocking
protocol HTTPClient: Sendable {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
struct MockHTTPClient: HTTPClient {
var result: Result<Data, Error>
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
let data = try result.get()
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
return (data, response)
}
}
// Usage in test
let mock = MockHTTPClient(result: .success(encodedUser))
let service = UserService(client: mock)
Test Structure (AAA)
@Test func createsOrderWithCorrectTotal() {
// Arrange
let items = [Item(price: 10), Item(price: 20)]
let cart = Cart(items: items)
// Act
let order = cart.checkout(tax: 0.1)
// Assert
#expect(order.subtotal == 30)
#expect(order.tax == 3)
#expect(order.total == 33)
}
Snapshot Testing (third-party)
// Using swift-snapshot-testing
import SnapshotTesting
class ViewSnapshotTests: XCTestCase {
func testButtonAppearance() {
let button = PrimaryButton(title: "Buy Now")
assertSnapshot(of: button, as: .image(on: .iPhone13))
}
}
Common Anti-Patterns
- Testing implementation details — test behavior and outputs, not private methods
- Shared mutable test state — reset in
setUp/tearDownor use isolated instances sleepin async tests — useawaitwith proper async APIs instead- Skipping tearDown — always clean up to avoid test pollution
- XCTAssert in new code — prefer
#expectwith the Swift Testing framework