The browser driver object should also not be created and closed in the test class but in the automation framework.
Having the browser driver object created in the setUp() method of each test class is redundant and error prone.
Dont create and close the driver object in the test class
Lets start with a simple test case for the Vancouver Public Library site:
- Open the home page of the site
- Execute a keyword search
- On the results page, click the title of the first result
- On the details page, check that the book title is correct (displayed and including more than 1 character)
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class TestCreateDriverInBaseClass {
String keyword = "java";
WebDriver driver;
@Before
public void setUp()
{ driver = new ChromeDriver();}
@After
public void tearDown() { driver.quit();}
@Test
public void testFirstResult() throws Exception
{
HomePage home = new HomePage(driver);
ResultsPage results = home.search(keyword);
DetailsPage details = results.selectResult(1);
assertTrue(details.correctBookTitle() == true);
}
}
The test class is very straightforward:
- uses 2 fields, one for the search keyword, the second for the driver object
- setUp() method for creating the driver object and opening the browser
- tearDown() method for closing the driver object and closing the browser
- one test script, testFirstResult(), that implements the test case
The typical test automation architecture uses the following layers:
In this architecture, each layer communicates only with the next layer so
- test scripts communicate only with the framework classes; test scripts do not communicate directly with the WebDriver API
- framework classes communicate with the WebDriver API; the framework classes are created using the page object model
- WebDriver API communicates with the browser
Our test script looks pretty good from the test automation architecture point of view:
- it uses the HomePage class for creating the home object
- the search() method of the HomePage class implements the keyword search; since the result of a keyword search is opening the results page, the search method returns an object of the ResultsPage class (it returns a page object)
- the selectResult() method of the ResultsPage class selects a result; since the outcome of clicking a result is going to the details page, the selectResult() method returns an object of the DetailsPage class
- the correctBookTitle() method of the DetailsPage class is used for checking if the book title is displayed and it has more than 0 characters
- the assertion checks if the book title is correct
The HomePage, ResultsPage and DetailsPage classes do not include WebDriver API either.
This is because all page object classes inherit from the Base Class.
All basic interactions with the site are implemented in the Base Class:
- open a page
- get a page title
- find an element using explicit wait
- get the value of an element
- check if an element is displayed
See the complete code below:
package CreateDriver;
import org.junit.Rule;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class BasePage
{
WebDriverWait wait;
WebDriver driver;
public BasePage(WebDriver driver)
{
this.driver = driver;
wait = new WebDriverWait(driver, 10);
}
public WebElement find(String locator)
{
return wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath(locator)));
}
public void clickElement(String locator)
{ find(locator).click();}
public String getPageTitle()
{ return driver.getTitle();}
public void open(String url)
{ driver.get(url);}
public void typeText(String locator, String keyword)
{
find(locator).sendKeys(keyword);
}
public String getValue(String locator)
{ return find(locator).getText();}
public Boolean isDisplayed(String locator)
{ return find(locator).isDisplayed(); }
}
public class HomePage extends BasePage
{
String searchFieldLocator = "//input[@id='globalQuery']";
String searchButtonLocator = "//input[@class='search_button']";
String siteUrl = "http://www.vpl.ca";
public HomePage(WebDriver driver) throws Exception
{
super(driver);
open(siteUrl);
if (getPageTitle().equalsIgnoreCase("Vancouver Public Library - Home") == false)
throw new Exception("this is not the home page");
}
public ResultsPage search(String keyword) throws Exception
{
typeText(searchFieldLocator, keyword);
clickElement(searchButtonLocator);
return new ResultsPage(driver);
}
}
public class ResultsPage extends BasePage
{
String resultLinkLocator = "(//a[@testid='bib_link'])";
public ResultsPage(WebDriver driver) throws Exception
{
super(driver);
if (getPageTitle().equalsIgnoreCase("Search | Vancouver Public Library | BiblioCommons") == false)
throw new Exception("this is not the results page");
}
public DetailsPage selectResult(int i) throws Exception
{
resultLinkLocator = resultLinkLocator + "[" + i + "]";
clickElement(resultLinkLocator);
return new DetailsPage(driver);
}
}
public class DetailsPage extends BasePage
{
String bookTitleElementLocator = "//h1[@id='item_bib_title']";
String bookAuthorElementLocator = "//a[@testid='author_search']";
public DetailsPage(WebDriver driver) throws Exception
{
super(driver);
if (getPageTitle().indexOf("Vancouver Public Library | BiblioCommons") < 0)
throw new Exception("this is not the details page");
}
public Boolean correctBookTitle()
{
return getValue(bookTitleElementLocator).length() > 0 &&
isDisplayed(bookTitleElementLocator);
}
}
The test class looks good with one exception:
creating and closing the driver object
We should create and close the driver object outside of the test class as well.
Create/close the driver in the page object classes
One option is to move the code that creates/closes the driver object from the test class to the page object classes.
This solves the problem of having the test scripts 100% free of WebDriver API but creates another issue.
Each page object class will have code for creating and closing the driver.
Having duplicated code for managing the driver is not a good idea.
Create/close the driver in base class constructor
Since the page object classes are using the base class for all site interactions, how about we move the driver code to the base class as well?
The first place where driver object code can go is in the the base class constructor.
The driver object is declared first as a member of the base class:
WebDriverWait wait;
WebDriver driver;
public BasePage()
{
driver = new ChromeDriver();
wait = new WebDriverWait(driver, 10);
}
This does not work unfortunately because the driver will be instantiated for each page object.
See what happens in our code:
- a driver object is created for the HomePage object; the browser is opened and the site is loaded in it
- when the ResultsPage object is created, another driver object is created; another browser instance is loaded but with no site in it; the site is still loaded on the first browser instance
What other options do we have?
Create a static driver member of the base class and instantiate it in a static block
First, we need the ability of creating the driver object once only and re-use it for all page objects.
The driver object should also be closed once at the end of the script.
The "create once only" reminds us of static class members:
protected static WebDriverWait wait;
protected static WebDriver driver;
If the driver is, however, still created in the base class constructor, things will not be very different.
But if the driver could be created before the base class constructor, then we are onto something.
So static blocks enter the scene.
The code from a static block is executed once only for the base class.
What is even better is that the static block code executes before the constructor:
package CreateDriver;
import org.junit.Rule;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class BasePage
{
protected static WebDriverWait wait;
p
rotected static WebDriver driver;
static
{
driver = new ChromeDriver();
wait = new WebDriverWait(driver, 10);
}
public static void closeBrowser()
{ driver.quit(); }
public WebElement find(String locator)
{
return wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath(locator)));
}
public void clickElement(String locator)
{
find(locator).click();
}
public String getPageTitle()
{
return driver.getTitle();
}
public void open(String url)
{
driver.get(url);
}
public void typeText(String locator, String keyword)
{ find(locator).sendKeys(keyword); }
public String getValue(String locator)
{ return find(locator).getText();}
public Boolean isDisplayed(String locator)
{ return find(locator).isDisplayed(); }
}
The test class looks a bit different with the new changes:
import CreateDriver.*;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TestCreateDriverInBaseClass
{
String keyword = "java";
@Test
public void testFirstResult() throws Exception
{
HomePage home = new HomePage();
ResultsPage results = home.search(keyword);
DetailsPage details = results.selectResult(1);
assertTrue(details.correctBookTitle() == true);
}
@Before
public void setUp() { }
@After
public void tearDown()
{ BasePage.closeBrowser();
}
}
Because the driver object is static and is initialized in the static block, it is created once for the Base class (for the HomePage object).
The ResultsPage and DetailsPage objects will use the same static object without initializing it again.
The constructors of the HomePage, ResultsPage and DetailsPages do not need the WebDriver parameter.
In the test class, there is no WebDriver member any longer.
The setUp() method is empty.
The tearDown() method uses a static method of the BasePage class that just closes the static driver.
This is excellent. In the setup method we could create the homepage object?? Rather then leave it empty?
ReplyDeleteKwabsQA
no, you should create the home page object in the test script.
DeleteHi??
ReplyDeleteHi Alex why is this ?
ReplyDeleteBecause the purpose of the setUp method is to initialize the test environment.
DeleteThe homepage object is part of a test script.
Hi...How is driver object shared if tests are run in parallel across multiple browsers?
ReplyDelete