Testing SwiftUI Views With ViewInspector
Written by Team Kodeco
ViewInspector is a powerful library for inspecting and testing SwiftUI views. This chapter demonstrates how to write tests for a simple to-do list app, showcasing how to use ViewInspector to verify the view hierarchy, modifiers and state.
Adding the ViewInspector Package to a SwiftUI Project
First, ensure your project has a unit testing suite. If it does not, add one by selecting File ▸ New ▸ Target and choosing the Unit Testing Bundle template.
Next, add ViewInspector to your project through Swift Package Manager. To access the Swift Package Manager, go to File ▸ Add Packages… Then, enter the following URL and click Add Package:
https://github.com/nalexn/ViewInspector
Make sure to add the package to your unit testing target and NOT your main app target.
Testing a To-Do List App
Start by creating a simple model for to-do items:
struct ToDoItem: Identifiable {
let id = UUID()
let title: String
var isCompleted = false
}
The view model will manage the items and the logic to add and toggle completion:
class ToDoListViewModel: ObservableObject {
@Published var items: [ToDoItem] = []
func addItem(_ title: String) {
items.append(ToDoItem(title: title))
}
func toggleCompletion(for item: ToDoItem) {
if let index = items.firstIndex(where: { $0.id == item.id }) {
items[index].isCompleted.toggle()
}
}
}
Now, let’s define the main ContentView
that renders the to-do list:
struct ContentView: View {
@StateObject var viewModel = ToDoListViewModel()
@State private var isAlertShowing = false
@State private var itemDescriptionInput = ""
var body: some View {
NavigationStack {
List {
ForEach(viewModel.items) { item in
HStack {
Text(item.title)
Spacer()
if item.isCompleted {
Image(systemName: "checkmark")
}
}
.onTapGesture { viewModel.toggleCompletion(for: item) }
}
}
.navigationTitle("ToDo List")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { isAlertShowing.toggle() }, label: { Image(systemName: "plus") })
}
}
.alert("Add a ToDo Item", isPresented: $isAlertShowing) {
TextField("Item Description", text: $itemDescriptionInput)
Button("Cancel", role: .cancel, action: clearInputs)
Button("OK", action: addItem)
}
}
}
private func addItem() {
viewModel.addItem(itemDescriptionInput)
clearInputs()
}
private func clearInputs() {
itemDescriptionInput = ""
}
}
Your Xcode preview should look like this:
This view includes an alert for adding new items, which is triggered by a button in the toolbar.
Testing with ViewInspector
We can write tests using ViewInspector to verify different aspects of the ContentView
:
import XCTest
import SwiftUI
import ViewInspector
@testable import ToDoListApp
class ContentViewTests: XCTestCase {
func testAddingItem() throws {
let viewModel = ToDoListViewModel()
viewModel.addItem("Buy milk")
let view = ContentView(viewModel: viewModel)
let list = try view.inspect().navigationStack().list()
XCTAssertEqual(list.count, 1)
let rowOneText = try list.forEach(0).hStack(0).text(0)
XCTAssertEqual(try rowOneText.string(), "Buy milk")
}
func testItemCompletion() throws {
let viewModel = ToDoListViewModel()
viewModel.addItem("Walk the dog")
let view = ContentView(viewModel: viewModel)
viewModel.toggleCompletion(for: viewModel.items.first!)
let rowOne = try view.inspect().navigationStack().list().forEach(0).hStack(0)
XCTAssertTrue(viewModel.items.first!.isCompleted)
XCTAssertEqual(try rowOne.image(2).actualImage(), Image(systemName: "checkmark"))
}
}
Let’s break down each test in detail:
1. testAddingItem
-
let viewModel = ToDoListViewModel()
initializes the view model that will be tested. -
viewModel.addItem("Buy milk")
adds a new item with the title “Buy milk” to the view model. -
let view = ContentView(viewModel: viewModel)
: Initializes theContentView
with the view model. -
let list = try view.inspect().navigationStack().list()
inspects the navigation stack to get the list within theContentView
. -
XCTAssertEqual(list.count, 1)
asserts that there is exactly one item in the list. -
let rowOneText = try list.forEach(0).hStack(0).text(0)
gets the text in the first row’s horizontal stack. -
XCTAssertEqual(try rowOneText.string(), "Buy milk")
asserts that the text is"Buy milk"
, as expected.
2. testItemCompletion
This test ensures that toggling the completion status of an item updates the view correctly.
-
let viewModel = ToDoListViewModel()
initializes the view model that will be tested. -
viewModel.addItem("Walk the dog")
adds a new item with the title"Walk the dog"
to the view model. -
let view = ContentView(viewModel: viewModel)
initializes theContentView
with the view model. -
viewModel.toggleCompletion(for: viewModel.items.first!)
toggles the completion status for the first item in the view model. -
let rowOne = try view.inspect().navigationStack().list().forEach- ).hStack(0)
inspects the navigation stack to get the horizontal stack in the first row. -
XCTAssertTrue(viewModel.items.first!.isCompleted)
asserts that the completion status for the first item in the view model is true. -
XCTAssertEqual(try rowOne.image(2).actualImage(), Image(systemName: "checkmark"))
asserts that the image in the horizontal stack is a checkmark, indicating completion.
ViewInspector provides a robust way to test SwiftUI views, filling a significant gap in SwiftUI testing capabilities. By following this example, you can start writing more comprehensive tests for your SwiftUI views and improve the reliability and maintainability of your SwiftUI apps. Happy testing!