Use the Debugger When Writing XCUITest Code

I prefer to use the debugger built into Xcode, lldb, when learning how to locate elements on the screen. I have the ability to display the elements tree for every screen in my app. From that I can see accessibility identifiers, labels, values for UI elements plus coordinates for each element. From this information, I am able to work out the path to each element. I can then directly enter queries on the command line which I can later use when implementing a screen class.

LLDB, Xcode’s Debugger Area

In Xcode, the debugger is active when your program stops execution at a breakpoint. The name for the window’s pane is the Debugger Area. Below, are several common command lines I use when I have hit a breakpoint. You can set a breakpoint on the first line of your test to get into the debugger.

Main screen for the app used for this post

Where I have a class var named app set to value of XCUIApplication()
po self.app . If there is no variable for XCUIApplication() then typing po XCUIApplication() also will output the same element tree as shown below.

Attributes: Application, pid: 75589, label: 'DemoAuto'
Element subtree:
 →Application, 0x600003e3cb60, pid: 75589, label: 'DemoAuto'
    Window (Main), 0x600003e3cd00, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x600003e3cdd0, {{0.0, 0.0}, {375.0, 667.0}}
        Button, 0x600003e3cea0, {{22.0, 80.0}, {136.0, 30.0}}, identifier: 'show-text-button', label: 'Show Label'
        StaticText, 0x600003e3ca90, {{22.0, 129.0}, {136.0, 21.0}}, identifier: 'show-text-label'
        Button, 0x600003e3c8f0, {{22.0, 226.0}, {136.0, 30.0}}, label: 'Open Settings'
        Button, 0x600003e3c9c0, {{22.0, 264.0}, {136.0, 30.0}}, identifier: 'open-safari-button', label: 'Open Safari'
        Button, 0x600003e3c820, {{36.0, 327.0}, {30.0, 30.0}}
        StaticText, 0x600003e3c680, {{22.0, 365.0}, {94.0, 21.0}}, label: 'No Identifier'
        TextField, 0x600003e3c750, {{16.0, 405.0}, {107.0, 30.0}}, value: Default Value
    Window, 0x600003e3c5b0, {{0.0, 0.0}, {375.0, 667.0}}
      StatusBar, 0x600003e3c4e0, {{0.0, 0.0}, {375.0, 20.0}}
        Other, 0x600003e3c410, {{0.0, 0.0}, {375.0, 20.0}}
        Other, 0x600003e3c340, {{0.0, 0.0}, {375.0, 20.0}}
          Other, 0x600003e3c270, {{6.0, 0.0}, {39.0, 20.0}}
          Other, 0x600003e3c1a0, {{50.0, 0.0}, {15.0, 20.0}}, label: '3 of 3 Wi-Fi bars', value: SSID
          Other, 0x600003e3c0d0, {{164.0, 0.0}, {51.0, 20.0}}, label: '8:05 AM'
          Other, 0x600003e3c000, {{335.0, 0.0}, {35.0, 20.0}}, label: '51% battery power'
Path to element:
 →Application, pid: 75589, label: 'DemoAuto'
Query chain:
 →Find: Target Application 'com.joeferrara.DemoAuto'
  Output: {
    Application, pid: 75589, label: 'DemoAuto'
  }

The main window for your app is shown in Window (Main), 0x600003e3cd00, {{0.0, 0.0}, {375.0, 667.0}}. The most useful information here is the frame for your main window. These screen coordinates are taken from an instance of the iPhone 8 simulator. The second window node is for the iOS screen which includes the status bar. Your UI test need not take this into account.

The UI elements for this bare bones single screen app are :

Window (Main), 0x600003e3cd00, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x600003e3cdd0, {{0.0, 0.0}, {375.0, 667.0}}
        Button, 0x600003e3cea0, {{22.0, 80.0}, {136.0, 30.0}}, identifier: 'show-text-button', label: 'Show Label'
        StaticText, 0x600003e3ca90, {{22.0, 129.0}, {136.0, 21.0}}, identifier: 'show-text-label'
        Button, 0x600003e3c8f0, {{22.0, 226.0}, {136.0, 30.0}}, label: 'Open Settings'
        Button, 0x600003e3c9c0, {{22.0, 264.0}, {136.0, 30.0}}, identifier: 'open-safari-button', label: 'Open Safari'
        Button, 0x600003e3c820, {{36.0, 327.0}, {30.0, 30.0}}
        StaticText, 0x600003e3c680, {{22.0, 365.0}, {94.0, 21.0}}, label: 'No Identifier'
        TextField, 0x600003e3c750, {{16.0, 405.0}, {107.0, 30.0}}, value: Default Value

Query an Element by Label

You can see that the first and fourth elements are buttons each with an accessibility identifier. It is best to locate an element with an identifier to make it likely that the element is unique and in cases where it is not unique, it is easy to see on the screen and possible to craft an appropriate query.

Executing Gestures with Debugger Variables

From the debugger command line, you can enter p XCUIApplication().buttons["show-test-button"]. If this is the first command used, then this text below is printed.

(XCUIElement) $R0 = 0x0000600003c4a060 {
  ObjectiveC.NSObject = {
    isa = XCUIElement
  }
}

To see what happens when you tap on this element you can then enter p $R2.tap() and observe the simulator. The text printed is below.

t =    52.69s     Tap "show-text-button" Button
    t =    52.69s         Wait for com.joeferrara.DemoAuto to idle
    t =    52.71s         Find the "show-text-button" Button
    t =    52.74s             Check for interrupting elements affecting "show-text-button" Button
    t =    52.74s         Synthesize event
    t =    52.85s         Wait for com.joeferrara.DemoAuto to idle

Any functions or variables in an XCUIElement class can be used from the command line. This is useful for changing the state on the screen. This allows for writing the test code in a live mode. You just need to make sure to copy the lines needed and incorporate into your code.

After tapping the element identifier by show-test-button, the sample app we’re using looks like this. The second label gets populated with the text “My Label”.

Tapping the Show Label causes text to appear in label below

Refresh Element Tree

Here is a way to refresh the element tree so it included the contents of the static text identified as show-text-label.
Use p XCUIApplications.windows.count so that the XCUITest proxy app requests all of the elements currently on the screen. Note that when you run a query then the framework will refresh its elements. I have seen some situation where that doesn’t happen automatically hence the usefulness of refreshing the elements tree explicitly. Also, as explained in the next paragraph, refreshing the element tree by getting a count of window does not require changing the app’s state.

If you are working with a more complex multi screen app then manually navigating through the app and issuing this command will update the element tree. This makes it possible to learn all of the screen elements for your app in one debugging session if desired.

The element tree line for the text label referenced above now reads :

StaticText, 0x600003e305b0, {{22.0, 129.0}, {136.0, 21.0}}, identifier: 'show-text-label', label: 'My Label'

Query an Element by Value

The lines for the Field element in the element tree looks like this :

TextField, 0x600003e3c750, {{16.0, 405.0}, {107.0, 30.0}}, value: Default Value

If you really wanted to do so, you could query for this element by using the value with XCUIApplication().values["Default Value"].

Query an Element by Path

The fourth button element has no identifier, label or value. The only way to access this element is by path.

p XCUIApplication().buttons.element(boundBy: 3)

Conclusion

In this post, I’ve given several examples of the things that can be done when using the Xcode command line debugger to support writing XCUITest tests. In addition to using the debugger while writing code, it is also very important to have some basic knowledge to help with debugging your code.

Resources

An in depth post on how to use the Xcode debugger for general coding is Dancing in the Debugger — A Waltz with LLDB. I read this a while back and it inspired me to use the debugger more often when writing test code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.