Testing is how you verify your code works correctly and keeps working as you make changes. Swift provides XCTest for unit and UI testing, and Swift 5.9+ introduced the new Swift Testing framework with improved syntax and features.
XCTest Basics
The traditional testing framework in Swift:
import XCTest
class CalculatorTests: XCTestCase {
var calculator: Calculator!
override func setUp() {
super.setUp()
calculator = Calculator()
}
override func tearDown() {
calculator = nil
super.tearDown()
}
func testAddition() {
let result = calculator.add(2, 3)
XCTAssertEqual(result, 5)
}
func testDivision() {
let result = calculator.divide(10, 2)
XCTAssertEqual(result, 5)
}
func testDivisionByZero() {
XCTAssertThrowsError(try calculator.divide(10, 0))
}
}
New Swift Testing Framework
Modern testing with better syntax (Swift 5.9+):
import Testing
@Suite
struct CalculatorTests {
let calculator = Calculator()
@Test
func addition() {
#expect(calculator.add(2, 3) == 5)
}
@Test
func subtraction() {
#expect(calculator.subtract(10, 3) == 7)
}
@Test
func divisionByZero() {
#expect(throws: DivisionError.self) {
try calculator.divide(10, 0)
}
}
}
Parameterized Tests
Test multiple inputs easily:
@Suite
struct ValidationTests {
@Test(arguments: [
("user@example.com", true),
("invalid-email", false),
("@example.com", false),
("user@.com", false)
])
func emailValidation(email: String, expectedValid: Bool) {
let isValid = EmailValidator.validate(email)
#expect(isValid == expectedValid)
}
}
Async Testing
Test asynchronous code:
@Suite
struct NetworkTests {
@Test
func fetchUser() async throws {
let user = try await NetworkManager.fetchUser(id: "123")
#expect(user.id == "123")
#expect(!user.name.isEmpty)
}
@Test
func timeout() async throws {
try await #require(timeout: .seconds(5)) {
try await slowOperation()
}
}
}
Test Suites and Organization
Organize tests into suites:
@Suite("Authentication")
struct AuthTests {
@Test("Login with valid credentials")
func validLogin() async throws {
let result = try await AuthService.login(
email: "test@example.com",
password: "password123"
)
#expect(result.isSuccess)
}
@Test("Login with invalid credentials")
func invalidLogin() async throws {
#expect(throws: AuthError.invalidCredentials) {
try await AuthService.login(
email: "test@example.com",
password: "wrong"
)
}
}
}
Testing SwiftUI Views
Test SwiftUI views and view models:
import Testing
import SwiftUI
@Suite
struct CounterViewTests {
@Test
func counterIncrement() {
let viewModel = CounterViewModel()
#expect(viewModel.count == 0)
viewModel.increment()
#expect(viewModel.count == 1)
}
@Test
func counterReset() {
let viewModel = CounterViewModel()
viewModel.increment()
viewModel.increment()
viewModel.reset()
#expect(viewModel.count == 0)
}
}
Mocking and Test Doubles
Create test doubles for dependencies:
protocol DataService {
func fetchData() async throws -> [String]
}
struct MockDataService: DataService {
var mockData: [String] = []
var shouldThrow = false
func fetchData() async throws -> [String] {
if shouldThrow {
throw NetworkError.serverError
}
return mockData
}
}
@Suite
struct ViewModelTests {
@Test
func loadDataSuccess() async throws {
let mockService = MockDataService(mockData: ["Item 1", "Item 2"])
let viewModel = DataViewModel(service: mockService)
try await viewModel.loadData()
#expect(viewModel.items.count == 2)
}
}
Performance Testing
Measure performance:
@Test
func performanceTest() {
measure {
// Code to measure
let result = complexCalculation()
}
}
Code Coverage
Enable code coverage in Xcode:
-
Edit Scheme → Test
-
Options → Code Coverage
-
Check "Gather coverage for all targets"
Best Practices
-
Test one thing - Each test should verify one behavior
-
Use descriptive names - Make test names clear
-
Arrange-Act-Assert - Structure tests clearly
-
Keep tests fast - Fast tests encourage frequent running
-
Mock external dependencies - Isolate units under test
-
Test edge cases - Don't just test happy paths
-
Keep tests independent - Tests shouldn't depend on each other
Wrap up
Swift provides powerful testing tools through both XCTest and the new Swift Testing framework. Writing tests improves code quality, catches bugs early, and makes refactoring safer.
Resources: