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.
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.
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.
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
SaveButtonis 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
SaveButtonbutton 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-selfaxis specifier. Axis specifiers tell Ranorex how to navigate the UI. The
descendent-or-selfaxis specifier is denoted as
//and effectively tells Ranorex to search for the specified button anywhere inside (or “under”) the
formelement. 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.
One way to improve the path performance is to avoid using the
descendent-or-selfaxis specifier. This path replaces
/toolbar/, which is a
childaxis specifier that tells Ranorex that the button must be inside a
toolbarelement. 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.
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
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
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
formelements 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
Titleproperty. 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.
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 (
formhas the same hierarchical structure as
If Ranorex identifies an element, but not its behavior (behaviors are represented by adapters), it will add generic tags such as
containerto 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
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,
@automationidis provided by the
UIAutomationadapter even though it’s being used with
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
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.
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.