Writing quality code in Swift applications requires testing. Write comprehensive tests with XCTest framework and the new Swift Testing Framework.
Unit Testing with XCTest
Simple Unit Test
swift
import XCTest
@testable import MyApp
class MathUtilsTests: XCTestCase {
func testAddition() {
// Given
let a = 5
let b = 3
// When
let result = MathUtils.add(a, b)
// Then
XCTAssertEqual(result, 8)
}
func testDivision_byZero_throwsError() {
XCTAssertThrowsError(try MathUtils.divide(10, by: 0))
}
}Setup and Teardown
swift
class UserViewModelTests: XCTestCase {
var viewModel: UserViewModel!
var mockService: MockUserService!
override func setUp() {
super.setUp()
mockService = MockUserService()
viewModel = UserViewModel(service: mockService)
}
override func tearDown() {
viewModel = nil
mockService = nil
super.tearDown()
}
func testLoadUsers_success() async throws {
// Given
let expectedUsers = [
User(id: "1", name: "John"),
User(id: "2", name: "Jane")
]
mockService.usersToReturn = expectedUsers
// When
try await viewModel.loadUsers()
// Then
XCTAssertEqual(viewModel.users.count, 2)
XCTAssertEqual(viewModel.users.first?.name, "John")
}
}Mocking
Manual Mock
swift
protocol UserServiceProtocol {
func fetchUsers() async throws -> [User]
}
class MockUserService: UserServiceProtocol {
var usersToReturn: [User] = []
var shouldThrowError = false
func fetchUsers() async throws -> [User] {
if shouldThrowError {
throw NetworkError.requestFailed
}
return usersToReturn
}
}Mocking with Dependency Injection
swift
class UserViewModel: ObservableObject {
@Published var users: [User] = []
private let service: UserServiceProtocol
init(service: UserServiceProtocol) {
self.service = service
}
func loadUsers() async throws {
users = try await service.fetchUsers()
}
}
// In test
class UserViewModelTests: XCTestCase {
func testLoadUsers() async throws {
let mockService = MockUserService()
let viewModel = UserViewModel(service: mockService)
mockService.usersToReturn = [
User(id: "1", name: "John")
]
try await viewModel.loadUsers()
XCTAssertEqual(viewModel.users.count, 1)
}
}UI Testing
Simple UI Test
swift
class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
app = XCUIApplication()
app.launchArguments = ["--uitesting"]
app.launch()
}
func testSuccessfulLogin() {
// Find and fill email field
let emailField = app.textFields["emailField"]
XCTAssertTrue(emailField.exists)
emailField.tap()
emailField.typeText("test@example.com")
// Find and fill password field
let passwordField = app.secureTextFields["passwordField"]
XCTAssertTrue(passwordField.exists)
passwordField.tap()
passwordField.typeText("password123")
// Tap login button
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.exists)
loginButton.tap()
// Verify successful login
let welcomeLabel = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 5))
}
}Performance Testing
swift
class PerformanceTests: XCTestCase {
func testJSONParsingPerformance() {
let jsonData = largeJSONData
measure {
_ = try? JSONDecoder().decode([User].self, from: jsonData)
}
}
func testSortingPerformance() {
let largeArray = Array(1...10000).shuffled()
measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) {
_ = largeArray.sorted()
}
}
}Swift Testing Framework (New)
The new testing framework introduced with Swift 6 offers more Swift-like syntax:
swift
import Testing
@Test("Addition should work correctly")
func addition() {
#expect(MathUtils.add(2, 3) == 5)
}
@Test("Division by zero should throw error")
func divisionByZero() {
#expect(throws: DivisionError.self) {
try MathUtils.divide(10, by: 0)
}
}
@Test("User list should load", arguments: [
(10, true),
(0, false),
(-1, false)
])
func userLoading(count: Int, shouldSucceed: Bool) async throws {
let service = MockUserService()
service.userCount = count
if shouldSucceed {
let users = try await service.fetchUsers()
#expect(users.count == count)
} else {
#expect(throws: Error.self) {
try await service.fetchUsers()
}
}
}Test Doubles
swift
// Spy
class SpyUserService: UserServiceProtocol {
var fetchUsersCalled = false
var fetchUsersCallCount = 0
func fetchUsers() async throws -> [User] {
fetchUsersCalled = true
fetchUsersCallCount += 1
return []
}
}
// Stub
class StubUserService: UserServiceProtocol {
func fetchUsers() async throws -> [User] {
return [
User(id: "1", name: "Test User")
]
}
}
// Fake
class FakeUserService: UserServiceProtocol {
private var users: [User] = []
func fetchUsers() async throws -> [User] {
return users
}
func addTestUser(_ user: User) {
users.append(user)
}
}TDD Practices
Red-Green-Refactor
swift
// 1. RED - Write test first
func testCalculateDiscount() {
let price = 100.0
let discount = 0.2
let result = PriceCalculator.calculateDiscount(
price: price,
discount: discount
)
XCTAssertEqual(result, 80.0, accuracy: 0.01)
}
// 2. GREEN - Write minimum code
struct PriceCalculator {
static func calculateDiscount(price: Double, discount: Double) -> Double {
return price * (1 - discount)
}
}
// 3. REFACTOR - Improve
struct PriceCalculator {
static func calculateDiscount(
price: Double,
discount: Double
) -> Double {
guard price > 0, discount >= 0, discount <= 1 else {
return price
}
return price * (1 - discount)
}
}Best Practices
- 1Arrange-Act-Assert: Organize tests in three phases
- 2Single responsibility: Each test should test one thing
- 3Independent: Tests should be independent of each other
- 4Fast: Unit tests should run fast
- 5Descriptive names: Test names should be descriptive
Testing with Swift is the key to writing reliable and maintainable code!