| name | ios-testing |
| description | iOS testing expert for XCTest and test automation. Use when writing unit tests, UI tests, performance tests, mocking, test doubles, or setting up test infrastructure. |
iOS Testing
Expert guidance for testing iOS applications with XCTest.
Unit Testing
Basic Test Structure
import XCTest
@testable import MyApp
final class UserTests: XCTestCase {
var sut: UserManager!
override func setUpWithError() throws {
try super.setUpWithError()
sut = UserManager()
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
func testUserCreation() {
let name = "John"
let email = "john@example.com"
let user = sut.createUser(name: name, email: email)
XCTAssertEqual(user.name, name)
XCTAssertEqual(user.email, email)
XCTAssertNotNil(user.id)
}
func testUserValidation_withInvalidEmail_shouldFail() {
let invalidEmail = "invalid-email"
let result = sut.validateEmail(invalidEmail)
XCTAssertFalse(result)
}
}
Testing Async Code
func testFetchUser_shouldReturnUser() async throws {
let userID = "123"
let user = try await sut.fetchUser(id: userID)
XCTAssertEqual(user.id, userID)
}
func testFetchUser_withInvalidID_shouldThrow() async {
let invalidID = "invalid"
do {
_ = try await sut.fetchUser(id: invalidID)
XCTFail("Expected error to be thrown")
} catch {
XCTAssertTrue(error is UserError)
}
}
Testing with Expectations
func testNotificationPosted() {
let expectation = expectation(forNotification: .userDidLogin, object: nil)
sut.login(username: "test", password: "password")
wait(for: [expectation], timeout: 1.0)
}
func testCallbackInvoked() {
let expectation = expectation(description: "Callback invoked")
sut.performAsync { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
}
Mocking
Protocol-Based Mocking
protocol UserServiceProtocol {
func fetchUser(id: String) async throws -> User
func saveUser(_ user: User) async throws
}
class MockUserService: UserServiceProtocol {
var fetchUserResult: Result<User, Error>?
var saveUserCalled = false
var savedUser: User?
func fetchUser(id: String) async throws -> User {
guard let result = fetchUserResult else {
fatalError("fetchUserResult not set")
}
return try result.get()
}
func saveUser(_ user: User) async throws {
saveUserCalled = true
savedUser = user
}
}
final class UserViewModelTests: XCTestCase {
var sut: UserViewModel!
var mockService: MockUserService!
override func setUp() {
mockService = MockUserService()
sut = UserViewModel(service: mockService)
}
func testLoadUser_shouldUpdateState() async {
let expectedUser = User(id: "1", name: "John")
mockService.fetchUserResult = .success(expectedUser)
await sut.loadUser(id: "1")
XCTAssertEqual(sut.user?.name, "John")
}
func testLoadUser_onError_shouldSetError() async {
mockService.fetchUserResult = .failure(UserError.notFound)
await sut.loadUser(id: "invalid")
XCTAssertNotNil(sut.error)
}
}
Spy Pattern
class SpyAnalytics: AnalyticsProtocol {
var trackedEvents: [(name: String, parameters: [String: Any])] = []
func track(event: String, parameters: [String: Any]) {
trackedEvents.append((event, parameters))
}
}
func testPurchase_shouldTrackEvent() async throws {
let spy = SpyAnalytics()
sut = PurchaseManager(analytics: spy)
try await sut.completePurchase(item: testItem)
XCTAssertEqual(spy.trackedEvents.count, 1)
XCTAssertEqual(spy.trackedEvents.first?.name, "purchase_completed")
}
UI Testing
Basic UI Test
import XCTest
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testLogin_withValidCredentials_shouldShowHome() {
let emailField = app.textFields["Email"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Login"]
emailField.tap()
emailField.typeText("test@example.com")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
}
func testLogin_withEmptyFields_shouldShowError() {
app.buttons["Login"].tap()
XCTAssertTrue(app.alerts["Error"].exists)
}
}
Accessibility Identifiers
TextField("Email", text: $email)
.accessibilityIdentifier("emailTextField")
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
let emailField = app.textFields["emailTextField"]
let loginButton = app.buttons["loginButton"]
Page Object Pattern
struct LoginPage {
let app: XCUIApplication
var emailField: XCUIElement {
app.textFields["emailTextField"]
}
var passwordField: XCUIElement {
app.secureTextFields["passwordTextField"]
}
var loginButton: XCUIElement {
app.buttons["loginButton"]
}
func login(email: String, password: String) {
emailField.tap()
emailField.typeText(email)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
}
}
struct HomePage {
let app: XCUIApplication
var welcomeLabel: XCUIElement {
app.staticTexts["welcomeLabel"]
}
var isDisplayed: Bool {
welcomeLabel.waitForExistence(timeout: 5)
}
}
func testLoginFlow() {
let loginPage = LoginPage(app: app)
let homePage = HomePage(app: app)
loginPage.login(email: "test@example.com", password: "password")
XCTAssertTrue(homePage.isDisplayed)
}
Performance Testing
Measure Performance
func testPerformance_listRendering() {
measure {
_ = sut.renderList(items: largeDataSet)
}
}
func testPerformance_withMetrics() {
let metrics: [XCTMetric] = [
XCTClockMetric(),
XCTMemoryMetric(),
XCTCPUMetric()
]
measure(metrics: metrics) {
performOperation()
}
}
func testPerformance_withBaseline() {
let options = XCTMeasureOptions()
options.iterationCount = 10
measure(options: options) {
sut.processData(data)
}
}
UI Performance
func testScrollPerformance() {
let app = XCUIApplication()
app.launch()
let list = app.tables.firstMatch
measure(metrics: [XCTOSSignpostMetric.scrollingAndDecelerationMetric]) {
list.swipeUp(velocity: .fast)
list.swipeDown(velocity: .fast)
}
}
Test Utilities
Test Helpers
extension XCTestCase {
func waitForAsync(timeout: TimeInterval = 1.0, action: @escaping () async throws -> Void) {
let expectation = expectation(description: "Async operation")
Task {
do {
try await action()
expectation.fulfill()
} catch {
XCTFail("Async operation failed: \(error)")
}
}
wait(for: [expectation], timeout: timeout)
}
}
Test Data Builders
struct UserBuilder {
private var id = UUID().uuidString
private var name = "Test User"
private var email = "test@example.com"
func withId(_ id: String) -> UserBuilder {
var builder = self
builder.id = id
return builder
}
func withName(_ name: String) -> UserBuilder {
var builder = self
builder.name = name
return builder
}
func withEmail(_ email: String) -> UserBuilder {
var builder = self
builder.email = email
return builder
}
func build() -> User {
User(id: id, name: name, email: email)
}
}
let user = UserBuilder()
.withName("John")
.withEmail("john@test.com")
.build()
Snapshot Testing
View Snapshot
import XCTest
@testable import MyApp
final class ViewSnapshotTests: XCTestCase {
func testProfileView_lightMode() {
let view = ProfileView(user: testUser)
.environment(\.colorScheme, .light)
let hostingController = UIHostingController(rootView: view)
hostingController.view.frame = UIScreen.main.bounds
assertSnapshot(matching: hostingController, as: .image)
}
}
Test Organization
Test Naming Convention
func test_login_withValidCredentials_shouldSucceed() { }
func test_login_withEmptyPassword_shouldFail() { }
func test_fetchUser_whenNetworkError_shouldReturnCachedData() { }
Test Categories
@Test(.tags(.integration))
func testDatabaseIntegration() { }
Apple Documentation