For some reason, when you write UI tests for a toggle on iOS/iPadOS, you have to fight. Yep, you can tap a toggle, but tapping in the middle is most of the time not helpful. Let see how this can be fixed as Apple doesn’t seems to be want to fix this.

Overview

You might have noticed that simply using tap() on a switch or toggle XCUIElement will not do a lot. I will show you a very basic project to illustrate how you can write a UI test for toggles that will work.

Tap is not working because the tap point will interact with the center of the view, which is most of the time empty or is the textual label.

Taping the center of a toggle does nothing

This project implements a basic view with two toggles: one is off, one is on. This app does nothing, the goal being to implement UI tests to check the toggles states, then change them and verify the result.

To make things work, I implemented a few functions as an XCTestCase extension to be able to find the proper tapping point at the switch center, interact with it, and check the toggle state.

NOTE:

You can get the project or browse the code from the public GitHub Repository.

As we are talking about UI tests, I will also give a small overview of my guidelines to write them.

Extension of XCTestCase

The extension is defined in the file XCTestCase+toggle.swift.

Checking the State of a Toggle

You check the state of a toggle in two different ways:

  1. synchronously when you expect the state to be already defined.
  2. by using an expectation in an asynchronous query.

You have two functions to check the current state of the toggle:

  • checkToggleIsOn(_ toggle: XCUIElement)
  • checkToggleIsOff(_ toggle: XCUIElement)

Both will make the test fail if the state does not match.

If you need to rely on expectation, for example after an action that should update it, then you can use either of the following:

  • waitForSwitchToBeOn(_ element: XCUIElement, timeOut: TimeInterval)
  • waitForSwitchToBeOff(_ element: XCUIElement, timeOut: TimeInterval)

Both are building a query based on a predicate and waiting for the expectation to be fulfilled in the given time interval.

Interact With a Toggle

To change the state of a toggle there are two functions, one per state to follow the same pattern:

  • turnSwitchOn(_ toggle: XCUIElement)
  • turnSwitchOff(_ toggle: XCUIElement)

Each function will proceed as follow:

  1. the tap coordinate is computed based on inner switch element inside the toggle.
  2. first, the toggle is tapped. I must admit I have no idea why this is needed, but without this intermediate tap, the next one will have no effect.
  3. tap on the computed coordinate, the center of the switch.
  4. Then call the corresponding function waitForSwitchTobeXxx()

You need to tap the center of the switch of a toggle

If anyone can explain to me why the first tap is required I will be more than happy to understand!

Implementing UI Tests

My test follows two core simple principles:

  • I only use accessibility identifiers to fetch elements in my UI from the tests.
  • Each view that I want to test is handled through an intermediate object: the view proxy.

Managing Accessibility Identifiers

You might ask why should you more rely on identifiers than element labels to write UI tests.

First, using labels will not work properly with localization as the label would change for each tested localization. Testing your app in a single language might hide some specific bugs and is probably not a good idea. Worse, having to adapt your UI tests to be able to test every language supported by your app is not a good idea if you plan to maintain your tests.

You could use tags to find elements, but they are just numbers. Managing collision between views will quickly be a nightmare, and you probably do not want to follow this path.

Using the accessibility identifier is a nice way to implement reliable and maintainable UI tests. And even if this is not enough, it’s also the first step to ensuring your app will be more accessible.

Implementing Accessibility Identifiers

In _SwiftUI, to define the accessibility identifier of a view you need to apply a dedicated modifier:

Toggle("Label for toggle", isOn: $toggleState)
  .accessibilityIdentifier("element-id")

Of course, you can hardcode the identifier in the code, but as you need to use the same one in your test it might be better to just share this through a string enumeration.

An easy implementation is to use a String enumeration to keep track of those ids.

To be able to share the enumeration between the application and test target, I follow a simple pattern:

If I need to test SomeView view then:

  • I define all sub-view identifiers in a SomeViewA11y file as an enum,
  • File is included both in the main target and UI-tests target

For a SomeView class, the accessibility identifier enumeration will be SomeViewA11y.

public enum SomeViewA11y: String {
  case element1 = "someView/element1"
  
}

I’m pretty sure there’s a better option than this basic pattern, but this does the job for me.

Don’t hesitate to propose an alternate implementation.

Implementing View Proxy

What do I call a proxy object?

My view proxy is simply a single way to access and manipulate my view from my tests.

It will contain:

  • accessors to the subviews,
  • check functions, when needed, to verify the view state,
  • action functions to manipulate it.

Why do I need a proxy for each view of my app?

Just because I don’t like to repeat code, especially technical code that could be subject to change. If I make an update to my view I know I need to update its proxy view, and probably do a bit of test.

As a thin abstraction layer of my view, I can limit the impact of changes in my view to avoid big updates to my UI tests.

The implementation

For a SomeView class, I will implement a SomeViewTestProxy.

import XCTest

public struct SomeViewTestProxy {
  /// define this if you want/need to target the app
  let app: XCUIApplication

  // If this is not the root view of the app,
  // reference the tested view to scope sub-element queries
  let view: XCUIElement

  /// Getter to a property
  var subView: XCUIElement {
    view.element(matching _some_type_, identifier: SomeViewA11y.subView.rawValue)
  }

  /// Check some state
  func checkSomething(...) { ... }

  /// An action on the view
  func performSomeAction(...) { ... }

  ...
}

This pattern could certainly be improved, but this is not the scope of this example project.

For my sample app, you can check ContentViewTestProxy in the project for the implementation details.

Starting the Test Case Implementation

My test case will restart the app for each test.

Each time a test is started I launch the app and keep the reference in a property.

final class suiToggleUITests: XCTestCase {
  
  var app: XCUIApplication?

  override func setUpWithError() throws {
    continueAfterFailure = false
    
    app = XCUIApplication()
    app?.launch()
  }
}

And when the test ends, I need to stop the app.

  override func tearDownWithError() throws {
    app?.terminate()
  }
}

Testing the current state

The first test would be the check the current state of one of the toggles.

So I create my proxy to manipulate my view and I call my checkToggleIsOff() function.

  func testPinInitialState() throws {
    let contentViewProxy = ContentViewTestProxy(app: app!)
    
    checkToggleIsOff(contentViewProxy!.pinToggle)
  }

Testing state change

Now that I’m sure about the initial state of my app, I can try to manipulate the toggle.

Before turning the toggle on it might be wise checking it’s actually off. This time I will not use the simple check method, but the one relying on expectation.

Using expectation is not useful, it’s just a way to illustrate the use of this function.

  func testTogglePinToOn() throws {
    let contentViewProxy = ContentViewTestProxy(app: app!)
    
    waitForSwitchToBeOff(contentViewProxy!.pinToggle, timeOut: 5)
    turnSwitchOn(contentViewProxy!.pinToggle)
  }

Remember that turnSwitchOn() will check the state was updated. So it’s not needed to add a call to waitForSwitchToBeOn().

Conclusion

Here it is. We have a set of simple functions to implement UI toggles tests.

I also gave you some hints on my way to implement UI tests that can scale and are quite easier to maintain.

Get the project and play with it. Of course, any feedback is welcome.

This code is not reusable as-is yet. This extension should be part of a simple framework. I’m working on it, but it’s not ready yet.