Page object model is a wonderful design pattern to abstract out Web Page elements and its actions from actual tests. This way tests do not have to worry about the creation/finding of WebElements. We use @FindBy and @FindAll annotations to mark a WebElement in the Page Object. @CacheLookup is a very important and overlooked annotation that can help us make our tests run faster.
@CacheLookup, as the name suggests helps us control when to cache a WebElement and when not to. This annotation, when applied over a WebElement, instructs Selenium to keep a cache of the WebElement instead of searching for the WebElement every time from the WebPage. This helps us save a lot of time.
In this tutorial, we will discuss about the benefits of @CacheLookup and will try to quantify the performance gains by using this annotation. However,this tutorial will not discuss the basics of @CacheLookup annotation.
Please go through the tutorial covering the basics of @CacheLookup here
Quick review of when Element lookup happens in Page Objects
Let us first understand when Selenium makes a FindElement call in PageObject model. Whenever you use a WebElement from your Page Object to perform some action a FindElement is triggered to lookup for the latest version of WebElement from the WebPage. This lookup is basically a FindElement REST Request to Browser's Web driver. This lookup is one of the most time-consuming section of your code.
Let us write a small test using the Page Object to see this interaction. We will log all the browser web driver interaction to validate this.
Below is the Test page URL
http://toolsqa.com/automation-practice-form/
In this test page, we will only create First and Last name text boxes. Here is the code
public class PracticeFormPageObject {
@FindBy(how = How.NAME, using = "firstname")
public WebElement firsName;
@FindBy(how = How.NAME, using = "lastname")
public WebElement lastName;
}
In the test we will use this Page object. The test does only two things
- Adds some text to First and Last name text boxes
- Reads the text from First and Last name text boxes
Here is the test code
@Test
public void TestFirstAndLastNameFields()
{
// In order to understand how action on WebElements using PageObjects work,
// we will save all the logs of chrome driver. Below statement helps us
// save all the logs in a file called TestLog.txt
System.setProperty("webdriver.chrome.logfile", "TestLog.txt");
ChromeDriver driver = new ChromeDriver();
driver.get("https://toolsqa.com/automation-practice-form/");
// Initialize the Page object
PracticeFormPageObject pageObject = PageFactory.initElements(driver, PracticeFormPageObject.class);
// Write some values to First and Last Name
pageObject.firsName.sendKeys("Virender"); // A FindBy call is triggered to fetch First Name
pageObject.lastName.sendKeys("Singh"); // A FindBy call is triggered to fetch Last Name
// Read values from the Text box.
pageObject.firsName.getText(); // A FindBy call is triggered to fetch First Name
pageObject.lastName.getText(); // A FindBy call is triggered to fetch Last Name
driver.close();
driver.quit();
}
In this test, there will four times when FindBy call will be triggered. This is marked in the code comments. Every statement like this "pageObject.firstName.getText()" are actually two calls
- FindBy to find the element
- getText to get the text
Both these calls are REST calls to the browser's WebDriver. If you run the above test you should get a log file named: TestLog.txt in the project's root directory. Let us open the text file and see the contents of it, for reference I am adding a trimmed version of the logs here
[12.654][INFO]: COMMAND FindElement {
"using": "name",
"value": "firstname"
}
[12.654][INFO]: Waiting for pending navigations...
[12.717][INFO]: Done waiting for pending navigations. Status: ok
[12.851][INFO]: Waiting for pending navigations...
[12.854][INFO]: Done waiting for pending navigations. Status: ok
[12.854][INFO]: RESPONSE FindElement {
"ELEMENT": "0.8984444413515806-1"
}
[12.860][INFO]: COMMAND TypeElement {
"id": "0.8984444413515806-1",
"value": [ "Virender" ]
}
[12.860][INFO]: Waiting for pending navigations...
[12.861][INFO]: Done waiting for pending navigations. Status: ok
[13.045][INFO]: Waiting for pending navigations...
[13.050][INFO]: Done waiting for pending navigations. Status: ok
[13.050][INFO]: RESPONSE TypeElement
[13.053][INFO]: COMMAND FindElement {
"using": "name",
"value": "lastname"
}
[13.053][INFO]: Waiting for pending navigations...
[13.054][INFO]: Done waiting for pending navigations. Status: ok
[13.074][INFO]: Waiting for pending navigations...
[13.082][INFO]: Done waiting for pending navigations. Status: ok
[13.082][INFO]: RESPONSE FindElement {
"ELEMENT": "0.8984444413515806-2"
}
[13.086][INFO]: COMMAND TypeElement {
"id": "0.8984444413515806-2",
"value": [ "Singh" ]
}
In the logs, we can see that there are pairs of FindElement and TypeElement calls for every statement write statement. Further investigation into the log file will show you that for every getText call you will have a FindElement and GetElementText calls.
This proves our case for the Page Object model that, for every action that we perform on a WebElement Selenium makes an additional lookup call. This call can turn out to be costly, as we will see in the below section, and can be avoided in most of the cases.
@CacheLookup and Performance analysis
Let us now see how we can avoid the additional lookup call and make our tests faster. The smart people from the Selenium WebDriver team knew about this problem and have a wonderful solution to it. They have provided us with @CacheLookup annotation. If a WebElement is decorated with this annotation, Selenium will not try to look up for the Web element on the Webpage, it will just return the cached version of the element. The cached version is created in the very first look up to the Web page, after the first lookup, all other lookup requests will be full filled by the cached element.
Let us perform a small test to see the time performance difference that we get for 1000 successive calls to a Cached and a Non-cached web element. This test will be performed on a single Element, First Name text box. The modified page object will look like this
public class PracticeFormModifiedPageObject {
@FindBy(how = How.NAME, using = "firstname")
public WebElement firsName;
@FindBy(how = How.NAME, using = "firstname")
@CacheLookup
public WebElement firsNameCached;
}
We will modify the Test code to measure the time taken by a cached and a non-cached web element to perform getText operation 1000 times. Here is the test code
public static void main(String[] args)
{
// In order to understand how action on WebElements using PageObjects work,
// we will save all the logs of chrome driver. Below statement helps us
// save all the logs in a file called TestLog.txt
System.setProperty("webdriver.chrome.logfile", "TestLog.txt");
ChromeDriver driver = new ChromeDriver();
driver.get("https://toolsqa.com/automation-practice-form/");
PracticeFormModifiedPageObject pageObject = PageFactory.initElements(driver, PracticeFormModifiedPageObject.class);
// set some text to fetch it later
pageObject.firstName.sendKeys("Virender");
// We will first try to get Text from the WebElement version which is not cached.
// We will measure the time to perform 1000 getText operations
long withoutCacheStartTime = System.currentTimeMillis();
for(int i = 0; i < 1000; i ++)
{
pageObject.firstName.getText();
}
long withoutCacheEndTime = System.currentTimeMillis();
System.out.println("Time take in seconds Without cache " + ((withoutCacheEndTime - withoutCacheStartTime)/ 1000));
// Let us now repeat the same process on the cached element and see
// the amount of time it takes to perform the same operation 1000 times
long withCacheStartTime = System.currentTimeMillis();
for(int i = 0; i < 1000; i ++)
{
pageObject.firsNameCached.getText();
}
long withCacheEndTime = System.currentTimeMillis();
System.out.println("Time take in seconds With cache " + ((withCacheEndTime - withCacheStartTime)/ 1000));
driver.close();
driver.quit();
}
Let us now run the test and see the output.
In the output, we can clearly see that the Cached version of WebElement takes 50 percent less time to perform the same operation compared to the Non Cached version. You could see a small variation in time mentioned above, this variation should be in effect of plus/minus 2 %.
When to use and when not to use @Cachelookup annotation
From the test above we can clearly see that using the Cached version of WebElement is beneficial, but is not true for every element. Let us try to understand these two important points.
Stale Element and Stale Element Exception
Even though it is tempting to use @CacheLookup annotation for every element, it is not suitable for elements that are dynamic in nature. By Dynamic elements, we mean the elements which refresh themselves quite often. For example, a Timer text which continuously changes the value every second. Another example could be a Stock price ticker which changes every few seconds. These elements are not a good candidates for @CacheLookup annotation.
The reason is quite simple, due to the fact that these elements change frequently on the web page, they are not good candidates for caching. Because if we cache one version of an element and it changes a few seconds later then we will get a Stale element exception.
Static elements
@CacheLookup is very useful for the elements that do not change on the web page once loaded. These types of elements constitute a majority of elements on the web page. So for those elements, as they will not change during test execution, we should use the @Cachelookup annotation to improve the test speed.
I hope this tutorial was helpful to you. Do drop me a feed back for any comments or issues with the content of this page. Before we leave, there is a small exercise for you.
- Analyze the content of the log file to verify if cached elements are making a call to browser's webdriver for FindElement