Chapters

Hide chapters

SwiftUI Cookbook

Live Edition · iOS 16.4 · Swift 5.8.1 · Xcode 14.3.1

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 FileNewTarget 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 FileAdd 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:

A simple to-do list app in SwiftUI.
A simple to-do list app in SwiftUI.

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 the ContentView with the view model.
  • let list = try view.inspect().navigationStack().list() inspects the navigation stack to get the list within the ContentView.
  • 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 the ContentView 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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.