Testing SwiftUI used to feel confusing — views are declarative, state is reactive, and async code is everywhere.
But now, testing SwiftUI is clean, predictable, and powerful, as long as you follow the right patterns.
This guide covers:
- testing ViewModels
- testing async/await
- mocking services
- dependency injection
- testing navigation
- testing SwiftUI views
- UI tests (XCUI)
- snapshot testing
Let’s level up your SwiftUI engineering discipline. 🚀
🧱 1. Test the ViewModel First (Always)
SwiftUI views are simple when ViewModels do the work.
Here’s a typical testable ViewModel:
@Observable
class ProfileViewModel {
var user: User?
var isLoading = false
let service: UserServiceProtocol
init(service: UserServiceProtocol) {
self.service = service
}
func load() async {
isLoading = true
defer { isLoading = false }
user = try? await service.fetchUser()
}
}
Test with a mock service:
struct MockUserService: UserServiceProtocol {
var userToReturn: User?
func fetchUser() async throws -> User {
return userToReturn!
}
}
Test file:
func test_profile_loads_user() async {
let mock = MockUserService(userToReturn: User(name: "Seb"))
let vm = ProfileViewModel(service: mock)
await vm.load()
XCTAssertEqual(vm.user?.name, "Seb")
XCTAssertFalse(vm.isLoading)
}
Rule:
📌 If your ViewModel is testable, your UI becomes trivial.
⚡ 2. Testing Async Code (Best Practice)
You no longer need expectations.
Just do:
func test_async_search() async {
let results = await search("swift")
XCTAssertEqual(results.count, 3)
}
Test cancellation:
func test_task_cancels() async {
let task = Task {
try await asyncWork()
}
task.cancel()
do {
_ = try await task.value
XCTFail("Should cancel")
} catch {
XCTAssertTrue(error is CancellationError)
}
}
Async testing is now beautifully simple.
🔌 3. Dependency Injection = Testability
Never do this:
class APIService {
let session = URLSession.shared // ❌ impossible to mock
}
Use protocols:
protocol APIClient {
func get(_ route: String) async throws -> Data
}
Inject:
init(api: APIClient) {
self.api = api
}
Mock it:
struct MockAPI: APIClient {
var response: Data
func get(_ route: String) async throws -> Data { response }
}
Now you can test anything.
🧪 4. Testing SwiftUI Views (The Right Way)
SwiftUI views are functions.
You test their logic, not screenshots.
Example:
struct GreetingView: View {
let name: String
var body: some View {
Text("Hello \(name)")
}
}
Test by inspecting the view:
Use the ViewInspector library (industry standard).
func test_greeting() throws {
let view = GreetingView(name: "Seb")
let text = try view.inspect().text().string()
XCTAssertEqual(text, "Hello Seb")
}
You can inspect:
- text
- buttons
- toggles
- navigation links
- bindings
- async action triggers
🔍 5. Navigation Tests
Example:
NavigationStack {
ContentView()
}
Test that a button triggers navigation:
try view.inspect().find(button: "Open Profile").tap()
let destination = try view.inspect().find(ProfileView.self)
XCTAssertNotNil(destination)
Navigation is now fully testable.
🧷 6. UI Tests (XCUI) — Best Practices
Basic structure:
func test_login_flow() {
let app = XCUIApplication()
app.launch()
app.textFields["email"].tap()
app.typeText("test@example.com")
app.secureTextFields["password"].tap()
app.typeText("password")
app.buttons["Login"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 2))
}
Rules for stable UI tests:
- give accessibility identifiers to every interactive element
- avoid relying on index-based queries
- use .waitForExistence
- keep login API mocked for UI tests
📸 7. Snapshot Testing (Optional, But Powerful)
Use the SnapshotTesting library:
assertSnapshot(of: MyView(), as: .image(layout: .device(config: .iPhone17)))
Great for:
- card UIs
- dashboards
- marketing screens -animations (as GIFs)
🧰 8. Full Test Folder Structure
Tests/
│
├── ViewModels/
│ ├── ProfileViewModelTests.swift
│ ├── HomeViewModelTests.swift
│
├── Services/
│ ├── MockAPI.swift
│
├── Views/
│ ├── GreetingViewTests.swift
│
├── UITests/
│ ├── LoginFlowTests.swift
│
└── Snapshots/
├── CardView.snapshot.png
Professional, scalable, clean.
🚀 Final Thoughts
Testing SwiftUI is:
- easy
- clean
- async-friendly
- mock-driven
- architecture-boosting
- team-ready
And it leads to:
- fewer regressions
- faster development
- safer refactoring
- more confidence
- more maintainable apps
Top comments (0)