Testing in Swift for Dummies : Part 2 — Running, Debugging, and Advanced Concepts

Edoardo Troianiello
4 min readJul 31, 2023

--

After creating our unit tests in Part 1, our next move is to run, debug, and delve deeper into more advanced testing techniques. Swift and Xcode provide a comprehensive suite of tools that make these steps efficient and informative. Let’s dive in!

Running Your Tests

Running tests is crucial to ensure your code performs as expected. Fortunately, Xcode provides a straightforward interface to do so.

Using Xcode:

  • Individual Tests: Navigate to the left sidebar in Xcode where you’ll see the diamond-shaped icons next to your test methods. These icons represent the test status: unrun (empty diamond), passed (green checkmark), or failed (red x). Click on a diamond to run the corresponding test.
  • All Tests in a Class: Click on the diamond next to the class name to run all tests within that class.
  • All Tests in the Project: Simply use the Cmd + U shortcut. This runs every test in your project, providing a comprehensive assessment.

Viewing Test Reports:

  • After running your tests, visit the Report Navigator in Xcode (icon looks like a speech bubble on the left sidebar). The latest test run will be highlighted, providing details on which tests passed, which failed, and how long each test took.

Debugging Failing Tests

A failing test isn’t necessarily a bad thing, it helps us pinpoint areas that need improvement. Here’s how to approach them:

  • Inspecting Failures: When a test fails, Xcode provides a red ‘x’ beside the test. Clicking this will guide you to the exact line causing the failure.
  • Using Breakpoints: If a test’s failure isn’t self-evident, breakpoints can be of immense help. By adding a breakpoint (clicking on the line number you want to inspect), you pause the test execution at that particular line. With the program paused, you can inspect variables, view the call stack, or step through code execution. To remove or disable a breakpoint, simply drag it out of the gutter or click on it.

Advanced Testing Concepts

Mocking and Stubbing:

As said in Part 1, when we test classes or functions that depend on external components, we don’t always want to use those actual components, especially if they make network calls or alter data. That’s where mocking and stubbing come into play.

  • Mocks: These are objects that mimic the behavior of real objects. They are used to isolate the unit of work from the remainder of the code to ensure that our tests are only testing what they’re supposed to. For instance, imagine we have a DataManager that fetches user data from a server. When testing a class that uses this manager, we don't want to make actual server calls. Instead, we can use a mock to simulate the behavior.
  • Stubs: These provide predetermined answers to method calls. They’re like mocks but don’t necessarily mimic the entire behavior, just parts of it.

Test-Driven Development (TDD):

In TDD, we cycle through three main steps:

  1. Red: Write a failing test.
  2. Green: Make the test pass by writing the necessary code.
  3. Refactor: Clean up your code while ensuring that the test still passes.

Example: Let’s say we want to add a method to our Calculator to calculate the factorial of a number. We start with the test:

func testFactorial() {
let result = calculator.factorial(5)
XCTAssertEqual(result, 120, "Factorial of 5 should be 120")
}

Initially, this test will fail because we haven’t implemented the factorial function. We then implement it to make the test pass and refactor if necessary.

Performance Testing:

Performance tests allow you to benchmark a portion of your code and track its performance over time.

Suppose you’ve created a sorting algorithm and you want to ensure its performance doesn’t degrade with code changes. Here’s how:

func testSortingPerformance() {
let array = [/* ... large array of numbers ... */]

self.measure {
_ = array.sorted()
}
}

This will measure the time taken to sort the array, and Xcode will help you track this over different test runs.

UI Testing:

UI tests simulate user interactions. For instance, if you have a button in your app that, when tapped, should navigate to a new screen, you can test this behavior:

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

let button = app.buttons["NextScreenButton"]
XCTAssertTrue(button.exists, "Button should be present")

button.tap()

let newScreenLabel = app.staticTexts["NewScreenLabel"]
XCTAssertTrue(newScreenLabel.exists, "Should navigate to the new screen")
}

This test launches the app, finds the button, taps it, and then checks if it navigated to the new screen by looking for a specific label.

UI Testing with SwiftUI

When writing UI tests for SwiftUI, the process is similar in many respects to writing tests for UIKit, but you might need to consider some specific SwiftUI nuances.

Accessibility Identifiers: For UI tests to interact with UI elements, those elements often need to have accessibility identifiers set. In SwiftUI, you can set these using the .accessibility(identifier:) modifier.

Button("Click Me") {
// Button action
}
.accessibility(identifier: "clickMeButton")

Interacting with Elements: Once accessibility identifiers are set, you can interact with these elements in your UI tests.

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

let button = app.buttons["clickMeButton"]
XCTAssertTrue(button.exists, "The button should be visible.")

button.tap()

// Assertions for post-tap behavior
}

Dealing with Dynamic Content: SwiftUI’s declarative nature means that UI might change more dynamically based on your app’s state. Always ensure that the expected state is set up appropriately before running your tests. This might involve mocking certain objects or setting the app in a particular mode.

Using Previews for Testing: One unique advantage with SwiftUI is the PreviewProvider. While it's primarily used for visual previews while coding, you can also utilize it to check different UI states quickly and decide on potential UI test scenarios.

Limitations:

  • GeometryReader: Elements positioned or sized using GeometryReader might sometimes be harder to test because they can change based on the screen's dimensions or other dynamic factors.
  • SwiftUI specific Components: Some new components in SwiftUI (like Picker) might have interactions different from their UIKit counterparts. Always test these thoroughly to ensure compatibility.

Conclusion

Testing is a fundamental aspect of software development. As your projects grow, tests ensure that new changes don’t introduce errors into existing functionalities.

--

--

Edoardo Troianiello

Computer Engineer | iOS Developer | Alumni @Apple Developer Academy in Naples