Testing SwiftUI
Write unit tests for view models and UI tests for SwiftUI views to validate behavior and flows.
Unit Test a ViewModel
Place business logic in a ViewModel and verify it using XCTestCase.
Syntax:
final class MyTests: XCTestCase { ... }- assertions like
XCTAssertEqual,XCTAssertTrue
Example
import SwiftUI
import Combine
final class CounterViewModel: ObservableObject {
@Published private(set) var count = 0
func increment() { count += 1 }
}
struct CounterView: View {
@StateObject private var vm = CounterViewModel()
var body: some View {
VStack(spacing: 12) {
Text("Count: \(vm.count)")
Button("Increment") { vm.increment() }
}
}
}
import SwiftUI
struct ContentView: View {
var body: some View { CounterView() }
}
import SwiftUI
@main
struct MyApp: App {
var body: some Scene { WindowGroup { ContentView() } }
}
import XCTest
final class CounterViewModelTests: XCTestCase {
func testIncrement() {
let vm = CounterViewModel()
vm.increment()
XCTAssertEqual(vm.count, 1)
}
}
This example verifies that calling increment() updates the published count.
Prerequisites (UI Testing Target)
- Add a UI Testing Bundle target (File → New → Target → UI Testing Bundle).
- Prefer locating elements by accessibility identifiers rather than visible text.
- Pass launch arguments and environment to control app state for tests (e.g., reset data, use fakes).
UI Test a SwiftUI View
Drive the app with XCUIApplication and locate elements by accessibility.
Syntax:
let app = XCUIApplication(); app.launch()app.buttons["Label"].tap()
Example
import SwiftUI
struct UITestDemo: View {
@State private var count = 0
var body: some View {
VStack(spacing: 12) {
Text("Count: \(count)").accessibilityIdentifier("countLabel")
Button("Increment") { count += 1 }
.accessibilityIdentifier("incrementButton")
}
}
}
import SwiftUI
struct ContentView: View {
var body: some View { UITestDemo() }
}
import SwiftUI
@main
struct MyApp: App {
init() {
let args = ProcessInfo.processInfo.arguments
let env = ProcessInfo.processInfo.environment
if args.contains("-ui-testing") || env["UITEST_RESET"] == "1" {
// Reset state, use mock stores/services, disable animations, etc.
// UIView.setAnimationsEnabled(false) can be used where applicable.
}
}
var body: some Scene { WindowGroup { ContentView() } }
}
import XCTest
final class MyUITests: XCTestCase {
func testIncrementButton() {
let app = XCUIApplication()
app.launchArguments = ["-ui-testing"]
app.launchEnvironment = ["UITEST_RESET": "1"]
app.launch()
let button = app.buttons["incrementButton"]
XCTAssertTrue(button.waitForExistence(timeout: 2))
button.tap()
let label = app.staticTexts["countLabel"]
XCTAssertTrue(label.waitForExistence(timeout: 2))
XCTAssertTrue(label.label.contains("Count: 1"))
}
}
This example launches the app, taps the button, and asserts the label exists via its accessibility identifier.
Tip: Keep logic in view models for easier unit testing; use UI tests for navigation and interaction flows.
Use waitForExistence to make tests more reliable and pass launch flags to set deterministic state.