Ranorex Speed #3: Optimizing RanoreXPaths

The key to optimizing RanoreXPath queries is to reduce the number of elements Ranorex has to evaluate as it scans the UI. This post shows a few ways to do that.

A RanoreXPath, or RXPath for short, describes the path to an element or set of elements. As I explain in my post on RanoreXPath basics, RanoreXPaths are essentially queries that tell Ranorex how to search the UI element hierarchy.

But not all paths are equal. They differ in scan speed, volatility, readability, and other aspects. When you record actions with Ranorex, it makes a best guess about the type of path you need. You can often improve your repository, and your test suite, by reviewing the changes Ranorex makes to your repository as you record your actions, and then optimizing them.

This is post #3 in a series of posts on optimizing Ranorex test run performance. If you’re not sure how elements and adapters, or RanoreXPaths and repositories relate to each other, you should read up on them before continuing.

Analyzing RanoreXPaths

Every element in the UI can be reached by any number of RanoreXPaths. Put differently, multiple queries can return the same (or partially overlapping) results. Our goal is to figure out how to construct the optimal path, but to do so, we have to understand how different factors affect RanoreXPath performance. Every path has advantages and disadvantages.

Suppose, for example, that your app is a window that has a toolbar with a number of buttons, and that the Name property of the first button is SaveButton. All of the following paths (and more) will return this button.

  1. /form//button[1]

    This path will find the first button in the app’s element hierarchy by its index position. This path is not resilient and will cause dependent tests to fail if the button order changes for any reason. In fact, it could fail even if the change is not in the toolbar. This can happen if the toolbar itself is moved, or if a button is added anywhere in the UI element hierarchy that happens to place it before the toolbar.

  2. /form//button[@name='SaveButton']

    This path is immune to most changes in order, so Ranorex will find the button even if new buttons are added or existing buttons are moved around. It is also far more readable and maintainable because the name SaveButton is much clearer than any index position. However, the path is not completely immune to every change in the UI. There might be more than one SaveButton button in the UI, and they may or may not have the same behavior.

    Both of these paths have an additional problem: they use the descendent-or-self axis specifier. Axis specifiers tell Ranorex how to navigate the UI. The descendent-or-self axis specifier is denoted as // and effectively tells Ranorex to search for the specified button anywhere inside (or “under”) the form element. This specifier is incredibly powerful because it lets Ranorex scan large swaths of the UI, so it can find the button almost anywhere. This power adds flexibility and makes it possible to write very short and relatively resilient RanoreXPaths. But it also has a downside: scanning elements is expensive.

  3. /form/toolbar/button[@name='SaveButton']

    One way to improve the path performance is to avoid using the descendent-or-self axis specifier. This path replaces // with /toolbar/, which is a child axis specifier that tells Ranorex that the button must be inside a toolbar element. Although the path is visibly longer, it is actually much more specific and constrains the search to buttons that are inside a toolbar. But this also means that the path is faster because far fewer elements will end up being scanned. In fact, while path #2 will scan every single button (and perhaps every element) in the UI, path #3 will likely end up scanning just a tiny fraction. Speed is often gained at the expense of flexibility.

  4. /form[@title='My App']/toolbar[@automationid='MainToolbar']/button[@name='SaveButton']

    This path adds additional filters to further improve performance. A filter – anything that appears in the square brackets – forces Ranorex to perform additional processing on each element as it scans it. The filters in this path are pretty simple, but filters can be much more complex and can use ands and ors and parentheses. But even with all this extra work, filters are crucial to improving performance.

    If there are multiple open applications, each of which contains toolbars with buttons, all the previous paths will have to scan every single app, significantly slowing Ranorex down. So filtering the forms constrains the scan to all the toolbars that are exclusively in the application being tested. Filtering the toolbars further constrains the scan to all the buttons that are exclusively in the MainToolbar toolbar.

    Combining all these filters has a cumulative effect and significantly reduces the overall number of elements that Ranorex has to scan. But this too has a cost: paths with too many filters are less resilient, though this can be mitigated to some extent. For example, Ranorex automatically generates filters for the form elements it adds to the repository while creating recordings, to constrain most scans to just the app under test. The filters usually constrain the form based on its Title property. But window titles often change over the lifetime of an app, so Ranorex uses a regular expression to match against the beginning of the title instead of a perfect match. This behavior relies on the assumption that even if the title changes, the first word or two will remain unchanged. This assumption allows Ranorex to generate partially resilient yet constraining paths. You will often have to strike a similar balance with the paths that you edit.

  5. /element[@title='My App']/element[@automationid='MainToolbar']/element[@name='SaveButton']

    Every element, regardless of its type, is still an element. This path is less optimal than path #4 because it doesn’t filter on the type of the element, but it serves to demonstrate the flexibility of RanoreXPath as a query language. If we consider the result, this path is likely very similar to path #4, mainly because of the attributes and the strict hierarchy (button under toolbar under form has the same hierarchical structure as element under element under element).

    If Ranorex identifies an element, but not its behavior (behaviors are represented by adapters), it will add generic tags such as element or container to the path. If you know better, you can simply change the type to any adapter supported by that element. In this case, the toolbar element can be represented by element, toolbar, or uiautomation (and possibly others). But you can actually mix and match attributes and tags, so you can use attributes provided by any adapter that can be used with the element, regardless of the tag specified in the path. In this example, @automationid is provided by the UIAutomation adapter even though it’s being used with element.

RanoreXPath Construction Strategies

Ranorex has a number of algorithms for constructing RanoreXPaths. These strategies are represented by the PathBuildMode enumeration and strike different balances between performance and flexibility. If you’re a coder, you can specify the PathBuildMode to use when you call the Element.GetPath() method.

For example, the Volatile strategy creates what are generally very fast and highly inflexible paths. It does this by adding many attributes to the filters, and by including non-deterministic and non-repeatable attributes such as window handles and process IDs that rarely appear twice. Ranorex does not optimize volatile paths, which essentially means that it doesn’t use the descendent-or-self and other flexible axis specifiers. In this case, the path is often faster because it has not been optimized.

On the other end of the spectrum is the StepCostReduce strategy that removes all the volatile attributes and intermediary elements that Ranorex guesses are unnecessary. This strategy creates somewhat slower paths that are far more flexible. It tries to make the paths unambiguous in an attempt to guarantee consistent runs. It does this by picking and choosing attributes based on other elements that are present in the element hierarchy at the time the path is constructed. The path might be ambiguous sometimes during a test run even if it was initially unambiguous.

Summary

Regardless of whether you write code or not, you should consider the paths that Ranorex generates as a starting point. Ranorex makes guesses; sometimes they’re great, and sometimes they’re not so great. Always review the generated paths and optimize them to suit your needs. And always remember the tradeoff between flexibility and speed.

4 Comments

 Add your comment
  1. great post! Thank you!

    one more question regarding the path #5:

    how can we force Ranorex skip these elements inbetween and generate a path only with automation id and axis specifiers?

    e.g.

    I want Ranorex generate following xpath

    /form[@automationid=”someform”]//container[@automationid=”somepage”]//button[@automationid=”somebutton”]

  2. Thank you for this post.

    I dont see a way to set a default behavior. Can Ranorex Spy and Ranorex Studio be forced to build only type #4 paths (PathBuildMode.Simple)?

    Best wishes

    • Hi Karsten,

      It is possible but not advisable.

      In Ranorex Studio, open your repository by double-clicking the .rxrep file in the Projects pane and then click the Settings button on the repository’s toolbar. On the Advanced tab, there’s a “RanoreXPath generation mode” property that lets you choose the default strategy. Note that it only supports the StepCostReduce, Reduce and Simple strategies. The other strategies (e.g., Volatile) are only available in code.

      There’s a similar Settings button on the Ranorex Spy’s toolbar.

      I should note that the Simple strategy does not optimize the paths and is usually not what you’re looking for. By not optimizing, it includes all of the elements in the path, which will create fast expressions. The problem is that those paths are not resilient. Every change in the UI, regardless of how insignificant, is likely to break your tests. The only advantage Simple has over Volatile is that Simple prefers repeatable, deterministic properties.

      There’s one thing I think I should clarify. The automatically generated paths are a good starting point but you should always edit your paths if you want to create a resilient performant test suite. There is a balance between performance and resilience that only you can strike. So the path generation mode is far less significant than the test author’s awareness of his/her needs.

Leave a Reply