Chapters

Hide chapters

SwiftUI by Tutorials

Second Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Building Blocks of SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

16. Testing & Debugging
Written by Bill Morefield

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Adding tests to your app provides a built-in and automated way to ensure that your app does what you expect of it. And not only do tests check that your code works as expected, but it’s also some assurance that future changes won’t break existing functionality.

In this chapter, you’ll learn how to implement UI tests in your SwiftUI app, and what to watch out for when testing your UI under this new paradigm.

Different types of tests

There are three types of tests that you’ll use in your apps. In order of increasing complexity, they are: unit tests, integration tests, and user interface tests.

The base of all testing, and the foundation of all other tests, is the unit test. Each unit test ensures that you get the expected output when a function processes a given input. Multiple unit tests may test the same piece of code, but each unit test itself should only focus on a single unit of code. A unit test should take milliseconds to execute. You’ll run them often, so you want them to run fast.

The next test up the testing hierarchy is the integration test. Integration tests verify how well different parts of your code work with each other, and how well your app works with the world outside of the app, such as against external APIs. Integration tests are more complex than unit tests; they usually take longer to run, and as a result, you’ll run them less often.

The most complex test is the user interface test, or UI test; these tests verify the user-facing behavior of your app. They simulate user interaction with the app and verify the user interface behaves as expected after responding to the interaction.

As you move up the testing hierarchy, each level of test checks a broader scope of action in the app. For example, a unit test would verify that the calculateTotal() method in your app returns the correct amount for an order. An integration test would verify that your app correctly determines that the items in the order are in stock. A UI test would verify that after adding an item to an order, the amount displayed to the user displays the correct value.

SwiftUI is a new visual framework, so this chapter focuses on how to write UI tests for SwiftUI apps. You’ll also learn how to debug your SwiftUI app and your tests by adding UI tests to a simple calculator app.

Debugging SwiftUI apps

Open the starter project for this chapter, and build and run the app; it’s a simple calculator. The app also supports Catalyst, so it works on iOS, iPadOS and the Mac. Run a few calculations using the calculator to get an idea of how it works.

Button(action: {
  if let val = Double(self.display) {
    self.memory = self.memory + val
    self.display = ""
    self.pendingOperation = .none
  } else {
    // Add Bug Fix Here
    self.display = "Error"
  }
}) {
  Text("M+")
    .frame(width: 45, height: 45)
  .addButtonBorder(Color.gray)
}

Setting breakpoints

To stop code during execution of an app, you set a breakpoint to tell the debugger to halt code execution when it reaches a particular line of code. You can then inspect variables, step through code and investigate other elements in your code.

Exploring breakpoint control

When stopped at a breakpoint, you’ll see a toolbar between the code editor and debug area. The first button in this toolbar toggles the visibility of the debug area. The second button disables all breakpoints but does not delete them. The third button continues the execution of the app. You can also select Debug ▸ Continue in the menu to continue app execution.

po self.memory

Adding UI tests

There’s a bug in this code you’ll notice when you Continue. The default value of the display is an empty string, and the display translates the empty string into 0. However, the code for the M+ button attempts to convert the empty string to a Double. When that conversion fails, the value Error appears to the user.

continueAfterFailure = false

Creating a UI Test

Proper test names should be precise and clear about what the test validates, since an app can end up with a large number of tests. Clear names make it easy to understand what failed. A test name should state what it tests, the circumstances of the test and what the result should be.

Accessing UI elements

Add the following code to the end of the test method:

let memoryButton = app.buttons["M+"]
memoryButton.tap()
Button, 0x600002498540, {{184.5, 102.5}, {45.0, 45.0}}, label: ’M+’

Reading the user interface

You found the M+ button by matching the label of the button. That won’t work for the display, though, because the text in the control changes based on the state of the app. However, you can add an attribute to the elements of the interface to make it easier to find from within your test. Open ContentView.swift. In the view, look for the two comments // Add display identifier and replace both with the following line:

.accessibility(identifier: "display")
// 1
let display = app.staticTexts["display"]
// 2
let displayText = display.label
// 3
XCTAssert(displayText == "0")

Fixing the bug

Open ContentView.swift, find the comment in the action for the M+ button that reads // Add Bug Fix Here, and change the next line to read:

self.display = ""

Adding more complex tests

Ideally, you would be building out your UI tests at the same time as you built out your UI. This way, as your UI becomes more fleshed out, your test suite will expand along with it. However, with the realities of modern development, you’ll usually be adding tests after the application already exists.

func testAddingTwoDigits() {
  let app = XCUIApplication()
  app.launch()

  let threeButton = app.buttons["3"]
  threeButton.tap()

  let addButton = app.buttons["+"]
  addButton.tap()

  let fiveButton = app.buttons["5"]
  fiveButton.tap()

  let equalButton = app.buttons["="]
  equalButton.tap()

  let display = app.staticTexts["display"]
  let displayText = display.label
  XCTAssert(displayText == "8")
}
XCTAssert(displayText == "8.0")

Simulating user interaction

You’ll first add a gesture so that swiping the memory display to the left clears it. The effect of the gesture works the same as tapping the MC key by setting the value of self.memory to zero.

let memorySwipe = DragGesture(minimumDistance: 20)
  .onEnded { _ in
    self.memory = 0.0
}
.gesture(memorySwipe)
.accessibility(identifier: "memoryDisplay")

func testSwipeToClearMemory() {
  let app = XCUIApplication()
  app.launch()

  let threeButton = app.buttons["3"]
  threeButton.tap()
  let fiveButton = app.buttons["5"]
  fiveButton.tap()

  let memoryButton = app.buttons["M+"]
  memoryButton.tap()

  let memoryDisplay = app.staticTexts["memoryDisplay"]
  // 1
  XCTAssert(memoryDisplay.exists)
  // 2
  memoryDisplay.swipeLeft()
  // 3
  XCTAssertFalse(memoryDisplay.exists)
}

Testing multiple platforms

Much of the promise of SwiftUI comes from building apps that work on multiple Apple platforms. Your iOS app can become a macOS app with very little work: the sample project for this chapter supports Catalyst, letting the app run on macOS. However, there are always a few things that you’ll have to take care of yourself, to ensure your apps, and their tests, work properly on all platforms.

#if !targetEnvironment(macCatalyst)
  // Test to exclude
#endif
#if !os(watchOS)
  // Your XCTest code
#endif

Key points

  • Building and debugging tests require a bit more attention due to the combination of code and user interface elements in SwiftUI.
  • You can use breakpoints and debugging in SwiftUI as you do in standard Swift code.
  • Tests automate checking the behavior of your code. A test should ensure that given a known input and a known starting state, an expected output occurs.
  • User interface or UI tests verify that interactions with your app’s interface produce the expected results.
  • Add an accessibilityIdentifer to elements that do not have static text for their label to improve location for testing.
  • You find all user interface elements from the XCUIApplication element used to launch the app in the test.
  • Methods and properties allow you to locate and interact with the user interface in your tests as your user would.
  • Different platforms often need different user interface tests. Use conditional compilation to match tests to the platform and operating system.

Challenge

As noted earlier, the swipe gesture to clear the memory does not work under Catalyst. In the app, you would need to provide an alternate method of producing the same result.

Challenge solution

You should begin by adding the new double-tap gesture. Change the current gesture definition to:

#if targetEnvironment(macCatalyst)
let doubleTap = TapGesture(count: 2)
  .onEnded { _ in
    self.memory = 0.0
}
#else
let memorySwipe = DragGesture(minimumDistance: 20)
  .onEnded { _ in
    self.memory = 0.0
}
#endif
#if targetEnvironment(macCatalyst)
Text("\(self.memory)")
  .accessibility(identifier: "memoryDisplay")
  .padding(.horizontal, 5)
  .frame(width: geometry.size.width * 0.85,
         alignment: .trailing)
  .overlay(RoundedRectangle(cornerRadius: 8)
           .stroke(lineWidth: 2)
           .foregroundColor(Color.gray))
  .gesture(doubleTap)
#else
Text("\(self.memory)")
  .accessibility(identifier: "memoryDisplay")
  .padding(.horizontal, 5)
  .frame(width: geometry.size.width * 0.85, 
         alignment: .trailing)
  .overlay(RoundedRectangle(cornerRadius: 8)
           .stroke(lineWidth: 2)
           .foregroundColor(Color.gray))
  .gesture(memorySwipe)
#endif
#if targetEnvironment(macCatalyst)
memoryDisplay.doubleTap()
#else
memoryDisplay.swipeLeft()
#endif

Where to go from here?

This chapter provided an introduction to testing and debugging your SwiftUI projects. Your starting point to go more in-depth should be Apple’s documentation on XCTest at https://developer.apple.com/documentation/xctest.

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.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now