swift-testing
Swift Testing Framework
Comprehensive guide to the modern Swift Testing framework, test organization, assertions, and Xcode Playgrounds for iOS 26 development.
Prerequisites
- Swift 6.0+ (included in Xcode 16+)
- Xcode 26+ recommended
Framework Overview
Swift Testing vs XCTest
| Feature | Swift Testing | XCTest |
|---|---|---|
| Test marking | @Test macro |
Method naming test* |
| Assertions | #expect, #require |
XCTAssert* |
| Test organization | Structs, actors, classes | XCTestCase subclass |
| Parallelism | Parallel by default | Process-based |
| Setup/Teardown | init/deinit |
setUp/tearDown |
Import
import Testing
@Test Macro
Basic Test
import Testing
@Test
func additionWorks() {
let result = 2 + 2
#expect(result == 4)
}
Test with Display Name
@Test("User can create account with valid email")
func createAccountWithValidEmail() async throws {
let account = try await AccountService.create(email: "test@example.com")
#expect(account.email == "test@example.com")
}
Async Tests
@Test
func fetchUserReturnsData() async throws {
let user = try await userService.fetch(id: "123")
#expect(user.name == "John Doe")
}
Throwing Tests
@Test
func invalidEmailThrows() throws {
#expect(throws: ValidationError.invalidEmail) {
try validate(email: "not-an-email")
}
}
Assertions
#expect
Basic expectations:
@Test
func basicExpectations() {
let value = 42
// Equality
#expect(value == 42)
// Inequality
#expect(value != 0)
// Boolean
#expect(value > 0)
// With message
#expect(value == 42, "Value should be 42")
}
#expect with Expressions
@Test
func expressionExpectations() {
let array = [1, 2, 3]
#expect(array.count == 3)
#expect(array.contains(2))
#expect(!array.isEmpty)
let optional: String? = "hello"
#expect(optional != nil)
}
#require
Unwrap optionals and fail fast:
@Test
func requireUnwrapping() throws {
let optional: String? = "hello"
// Unwrap or fail test
let value = try #require(optional)
#expect(value == "hello")
}
@Test
func requireCondition() throws {
let array = [1, 2, 3]
// Fail if condition is false
try #require(array.count > 0)
let first = try #require(array.first)
#expect(first == 1)
}
Testing Throws
@Test
func throwingBehavior() {
// Expect any error
#expect(throws: (any Error).self) {
try riskyOperation()
}
// Expect specific error type
#expect(throws: NetworkError.self) {
try fetchData()
}
// Expect specific error value
#expect(throws: NetworkError.timeout) {
try fetchWithTimeout()
}
}
@Test
func noThrow() {
// Expect no error
#expect(throws: Never.self) {
safeOperation()
}
}
Custom Failure Messages
@Test
func customMessages() {
let user = User(name: "Alice", age: 25)
#expect(user.age >= 18, "User must be an adult, but age was \(user.age)")
}
Test Organization
Test Suites with Structs
@Suite("User Authentication Tests")
struct AuthenticationTests {
@Test("Valid credentials succeed")
func validLogin() async throws {
let result = try await auth.login(user: "test", pass: "password")
#expect(result.success)
}
@Test("Invalid credentials fail")
func invalidLogin() async throws {
let result = try await auth.login(user: "test", pass: "wrong")
#expect(!result.success)
}
}
Nested Suites
@Suite("API Tests")
struct APITests {
@Suite("User Endpoints")
struct UserEndpoints {
@Test func getUser() async { }
@Test func createUser() async { }
}
@Suite("Post Endpoints")
struct PostEndpoints {
@Test func getPosts() async { }
@Test func createPost() async { }
}
}
Using Actors for Isolation
@Suite
actor DatabaseTests {
var database: TestDatabase
init() async throws {
database = try await TestDatabase.create()
}
@Test
func insertWorks() async throws {
try await database.insert(User(name: "Test"))
let count = try await database.count(User.self)
#expect(count == 1)
}
}
Setup and Teardown
@Suite
struct DatabaseTests {
let database: Database
init() async throws {
// Setup - called before each test
database = try await Database.createInMemory()
try await database.migrate()
}
deinit {
// Teardown - called after each test
// Note: async cleanup should be done differently
}
@Test
func testInsert() async throws {
try await database.insert(item)
#expect(try await database.count() == 1)
}
}
Parameterized Tests
Basic Parameters
@Test("Validation", arguments: [
"test@example.com",
"user@domain.org",
"name@company.co.uk"
])
func validEmails(email: String) {
#expect(isValidEmail(email))
}
Multiple Arguments
@Test("Addition", arguments: [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300)
])
func addition(a: Int, b: Int, expected: Int) {
#expect(a + b == expected)
}
Zip Arguments
@Test(arguments: zip(
["hello", "world", "test"],
[5, 5, 4]
))
func stringLength(string: String, expectedLength: Int) {
#expect(string.count == expectedLength)
}
Custom Types as Arguments
struct TestCase: CustomTestStringConvertible {
let input: String
let expected: Int
var testDescription: String {
"'\(input)' should have length \(expected)"
}
}
@Test("String lengths", arguments: [
TestCase(input: "hello", expected: 5),
TestCase(input: "", expected: 0),
TestCase(input: "Swift", expected: 5)
])
func stringLength(testCase: TestCase) {
#expect(testCase.input.count == testCase.expected)
}
Test Traits
.serialized
Run tests sequentially:
@Suite(.serialized)
struct OrderDependentTests {
@Test func step1() { }
@Test func step2() { }
@Test func step3() { }
}
.disabled
Skip tests:
@Test(.disabled("Known bug, see issue #123"))
func brokenFeature() {
// Won't run
}
@Test(.disabled(if: isCI, "Flaky on CI"))
func flakyTest() {
// Conditionally disabled
}
.enabled
Conditionally enable:
@Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_SLOW_TESTS"] != nil))
func slowIntegrationTest() async throws {
// Only runs when environment variable is set
}
.tags
Organize with tags:
extension Tag {
@Tag static var critical: Self
@Tag static var slow: Self
@Tag static var integration: Self
}
@Test(.tags(.critical))
func criticalFeature() { }
@Test(.tags(.slow, .integration))
func slowIntegrationTest() async { }
.timeLimit
Set execution limit:
@Test(.timeLimit(.seconds(5)))
func mustCompleteQuickly() async throws {
// Fails if takes more than 5 seconds
}
.bug
Reference known issues:
@Test(.bug("https://github.com/org/repo/issues/123", "Expected failure"))
func knownIssue() {
// Test expected to fail
}
Parallel Execution
Default Parallel
Tests run in parallel by default:
@Suite
struct ParallelTests {
// These run concurrently
@Test func test1() async { }
@Test func test2() async { }
@Test func test3() async { }
}
Serial When Needed
@Suite(.serialized)
struct SerialTests {
static var sharedState = 0
@Test func first() {
Self.sharedState = 1
#expect(Self.sharedState == 1)
}
@Test func second() {
Self.sharedState = 2
#expect(Self.sharedState == 2)
}
}
Mocking and Test Doubles
Protocol-Based Mocking
protocol UserService {
func fetch(id: String) async throws -> User
}
struct MockUserService: UserService {
var userToReturn: User?
var errorToThrow: Error?
func fetch(id: String) async throws -> User {
if let error = errorToThrow {
throw error
}
guard let user = userToReturn else {
throw TestError.notConfigured
}
return user
}
}
@Suite
struct UserViewModelTests {
@Test
func fetchUserSuccess() async throws {
var mockService = MockUserService()
mockService.userToReturn = User(id: "1", name: "Test")
let viewModel = UserViewModel(service: mockService)
try await viewModel.loadUser(id: "1")
#expect(viewModel.user?.name == "Test")
}
}
Spy for Verification
final class SpyUserService: UserService {
var fetchCallCount = 0
var lastFetchedId: String?
func fetch(id: String) async throws -> User {
fetchCallCount += 1
lastFetchedId = id
return User(id: id, name: "Test")
}
}
@Test
func loadsUserOnAppear() async throws {
let spy = SpyUserService()
let viewModel = UserViewModel(service: spy)
await viewModel.loadUser(id: "123")
#expect(spy.fetchCallCount == 1)
#expect(spy.lastFetchedId == "123")
}
Testing SwiftUI
Testing Observable ViewModels
@Observable
class CounterViewModel {
var count = 0
func increment() {
count += 1
}
}
@Suite
struct CounterViewModelTests {
@Test
func incrementIncreasesCount() {
let viewModel = CounterViewModel()
viewModel.increment()
#expect(viewModel.count == 1)
}
@Test
func multipleIncrements() {
let viewModel = CounterViewModel()
viewModel.increment()
viewModel.increment()
viewModel.increment()
#expect(viewModel.count == 3)
}
}
Testing Async ViewModels
@Observable
@MainActor
class UserListViewModel {
var users: [User] = []
var isLoading = false
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUsers() async {
isLoading = true
defer { isLoading = false }
users = (try? await service.fetchAll()) ?? []
}
}
@Suite
struct UserListViewModelTests {
@Test
@MainActor
func loadUsersPopulatesArray() async {
var mock = MockUserService()
mock.usersToReturn = [User(id: "1", name: "Alice")]
let viewModel = UserListViewModel(service: mock)
await viewModel.loadUsers()
#expect(viewModel.users.count == 1)
#expect(viewModel.isLoading == false)
}
}
Migration from XCTest
Side-by-Side
Both frameworks can coexist:
// XCTest
import XCTest
class LegacyTests: XCTestCase {
func testOldStyle() {
XCTAssertEqual(2 + 2, 4)
}
}
// Swift Testing
import Testing
@Test
func newStyle() {
#expect(2 + 2 == 4)
}
Mapping Assertions
| XCTest | Swift Testing |
|---|---|
XCTAssertTrue(x) |
#expect(x) |
XCTAssertFalse(x) |
#expect(!x) |
XCTAssertEqual(a, b) |
#expect(a == b) |
XCTAssertNil(x) |
#expect(x == nil) |
XCTAssertNotNil(x) |
try #require(x) |
XCTAssertThrowsError |
#expect(throws:) |
XCTUnwrap(x) |
try #require(x) |
What to Keep in XCTest
- Performance tests (
measure {}) - UI tests (XCUITest)
- Existing stable test suites
Xcode Playgrounds
#Playground Macro (iOS 26)
import SwiftUI
#Playground {
let greeting = "Hello, Playgrounds!"
print(greeting)
}
#Playground("SwiftUI Preview") {
struct ContentView: View {
var body: some View {
Text("Hello, World!")
}
}
ContentView()
}
Named Playground Blocks
#Playground("Data Processing") {
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }
print(doubled)
}
#Playground("API Simulation") {
struct User: Codable {
let name: String
}
let json = #"{"name": "Alice"}"#
let user = try? JSONDecoder().decode(User.self, from: json.data(using: .utf8)!)
print(user?.name ?? "Unknown")
}
SwiftUI in Playgrounds
#Playground("Interactive UI") {
struct Counter: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}
Counter()
}
Best Practices
1. Descriptive Test Names
// GOOD
@Test("User cannot login with expired token")
func expiredTokenLogin() { }
// AVOID
@Test
func test1() { }
2. One Assertion Focus
// GOOD: Focused test
@Test
func userNameIsCapitalized() {
let user = User(name: "alice")
#expect(user.displayName == "Alice")
}
// AVOID: Multiple unrelated assertions
@Test
func userTests() {
let user = User(name: "alice")
#expect(user.displayName == "Alice")
#expect(user.email != nil)
#expect(user.createdAt <= Date())
}
3. Use Structs for Test Suites
// GOOD: Struct-based, each test gets fresh instance
@Suite
struct UserTests {
let service = UserService()
@Test func fetch() { }
@Test func create() { }
}
4. Parameterize Repetitive Tests
// GOOD: Parameterized
@Test(arguments: ["", " ", " "])
func emptyStringsAreInvalid(input: String) {
#expect(!isValid(input))
}
// AVOID: Duplicated tests
@Test func emptyIsInvalid() { #expect(!isValid("")) }
@Test func spaceIsInvalid() { #expect(!isValid(" ")) }
@Test func spacesAreInvalid() { #expect(!isValid(" ")) }
5. Use Tags for Organization
extension Tag {
@Tag static var unit: Self
@Tag static var integration: Self
@Tag static var slow: Self
}
// Filter in Xcode or command line
// swift test --filter "unit"
Official Resources
More from bluewaves-creations/bluewaves-skills
photographer-testino
Generate images in Mario Testino's glamorous vibrant style. Use when users ask for Testino style, high fashion glamour, bold saturated colors, warm luxurious photography, dynamic sensual energy.
35photographer-lindbergh
Generate images in Peter Lindbergh's iconic black and white style. Use when users ask for Lindbergh style, raw authentic beauty, emotional B&W portraits, supermodel aesthetic, or unretouched natural photography.
30photographer-lachapelle
Generate images in David LaChapelle's surreal pop art style. Use when users ask for LaChapelle style, pop surrealism, hyper-saturated colors, theatrical staging, baroque maximalism, kitsch aesthetic.
24epub-creator
Create production-quality EPUB 3 ebooks from markdown and images with automated QA, formatting fixes, and validation. Use when creating ebooks, converting markdown to EPUB, or compiling chapters into a publishable book. Handles markdown quirks, generates TOC, adds covers, and validates output automatically.
22photographer-vonunwerth
Generate images in Ellen von Unwerth's playful vintage style. Use when users ask for von Unwerth style, playful sensuality, vintage film noir, whimsical feminine photography, retro glamour, narrative storytelling.
19photographer-ritts
Generate images in Herb Ritts' sculptural black and white style. Use when users ask for Ritts style, classical Greek aesthetic, sculptural body photography, California golden hour, minimalist athletic portraits.
18