Write test scripts that express the user intent instead of how the user interacts with the page
When you write a test case, does it look like this?
open home page of the site
verify that home page url is correct
click search box
type keyword in search box
click search button
verify that results page url is correct
click sort order list
select “year ascending” value
verify that “year ascending” is selected
get all results displayed in the page
verify that the results are sorted by year, ascending
or like this
open home page of the site
verify that home page is displayed
search for keyword
verify that results page is displayed
change sort order to “year ascending”
verify that all results are sorted by year, ascending
I prefer the second type of test case because
it is shorter
it expresses everything from the point of view of the user
it leaves aside how different things are implemented
The first type of test case is too long as it focuses on the interaction with the page too much. It is created at a very low level.
Our test script is written at a low level as of now:
@Test
public void canFilterByBooksTest() {
HomePage homePage = new HomePage(driver);
homePage.open();
Verify.verify(homePage.isDisplayed(), "home page is not displayed!");
homePage.typeKeywordInSearchBox(KEYWORD);
homePage.clickSearchButton();
ResultsPage resultsPage = new ResultsPage(driver);
Verify.verify(resultsPage.isDisplayed(), "results page is not displayed!");
Verify.verify(resultsPage.getResultsFoundValue().contains("1 to 10"),
"results found label does not include 1 to 10!");
Assert.assertEquals(resultsPage.getBookFilterEnabledAttribute(), "false");
resultsPage.clickBookFilter();
Assert.assertEquals(resultsPage.getBookFilterEnabledAttribute(), "true");
resultsPage.clickBookFilter();
Assert.assertEquals(resultsPage.getBookFilterEnabledAttribute(), "false");
}
When the test script is written at a low level, it focuses on the interaction with the page. This is clear from the fact that the script interacts with all page elements that are required by the test case. It is also clear from the names of the methods:
typeKeywordInSearchBox() - types a keyword in the search box element
clickSearchButton() - clicks the search button element
getResultsFoundValue() - gets the value of the results found element
getBookFilterEnabledAttribute() - gets the attribute of the book filter element
clickBookFilter() - clicks the book filter element
All these methods are fairly small in the page classes and interact with one element only. They usually do only 2 things:
find the element and interact with it (by clicking, typing, selecting, etc) or
find the element and get a value out of it
public String getResultsFoundValue() {
WebElement label = driver.findElement(RESULTS_FOUND_XPATH);
return label.getText();
}
public String getBookFilterEnabledAttribute() {
WebElement filter = driver.findElement(BOOK_FILTER_XPATH);
return filter.getAttribute("aria-checked");
}
public void clickBookFilter() {
WebElement filter = driver.findElement(BOOK_FILTER_XPATH);
filter.click();
}
How do we make the transition from a test focused on the interaction with the page to a test that shows the user intent?
Look at the test case from the beginning of this post. The second type of test case uses user actions such as searching or changing sort order. Searching is done by interacting with 2 elements (search box and search button). Changing sort order is done by interacting with the sort listbox.
So, this is what we will do.
We will make the following changes to the HomePage class:
create a searchBy() method in the HomePage class that relies on the typeKeywordInSearchBox() and clickSearchButton() methods
change the typeKeywordInSearchBox() and clickSearchButton() methods from public to private so they can only be used in the HomePage class
rename typeKeywordInSearchBox() to typeKeyword()
public class HomePage {
private WebDriver driver;
private static final String HOME_PAGE_URL = "https://www.vpl.ca/";
private static final By SEARCH_BOX_ID = By.id("edit-search");
private static final By SEARCH_BUTTON_ID = By.id("edit-submit");
public HomePage(WebDriver driver) {
this.driver = driver;
}
public void open() {
driver.get(HOME_PAGE_URL);
}
public boolean isDisplayed() {
return getUrl().equalsIgnoreCase(HOME_PAGE_URL);
}
public String getUrl() {
return driver.getCurrentUrl();
}
public void searchBy(String keyword) {
typeKeyword(keyword);
clickSearchButton();
}
private void typeKeyword(String keyword) {
WebElement searchBox = driver.findElement(SEARCH_BOX_ID);
searchBox.sendKeys(keyword);
}
private void clickSearchButton() {
WebElement searchButton = driver.findElement(SEARCH_BUTTON_ID);
searchButton.click();
}
}
Also, change a few things in the ResultsPage class as well:
rename getResultsFoundValue() to getResultsFound()
rename getBookFilterEnabledAttribute() to getBookFilterState(); do the same thing for the similar methods of the other 2 filter
change getBookFilterEnabledAttribute() so it returns a boolean value instead of a String; similar changes for the getComicBookFilterEnabledAttribute() and getEBookFilterEnabledAttribute()
rename clickBookFilter() to checkBookFilter(); do the same thing for the similar methods of the other 2 filters
public class ResultsPage {
private WebDriver driver;
private static final String RESULTS_PAGE_URL =
"https://vpl.bibliocommons.com";
private static final By RESULTS_FOUND_XPATH =
By.xpath("(//span[@data-key = 'pagination-text'])[1]");
private static final By BOOK_FILTER_XPATH =
By.xpath("//button[@aria-labelledby='label_FORMAT-BOOKS-BK']");
private static final By COMIC_BOOK_FILTER_XPATH =
By.xpath("//button[@aria-labelledby='label_FORMAT-BOOKS-COMIC_BK']");
private static final By E_BOOK_FILTER_XPATH =
By.xpath("//button[@aria-labelledby='label_FORMAT-BOOKS-EBOOK']");
public ResultsPage(WebDriver driver) {
this.driver = driver;
}
public String getUrl() {
return driver.getCurrentUrl();
}
public boolean isDisplayed() {
return getUrl().startsWith(RESULTS_PAGE_URL);
}
public String getResultsFound() {
WebElement label = driver.findElement(RESULTS_FOUND_XPATH);
return label.getText();
}
public boolean getBookFilterState() {
WebElement filter = driver.findElement(BOOK_FILTER_XPATH);
String attribute = filter.getAttribute("aria-checked");
return Boolean.parseBoolean(ariaChecked);
}
public void checkBookFilter() {
WebElement filter = driver.findElement(BOOK_FILTER_XPATH);
filter.click();
}
public boolean getComicBookFilterState() {
WebElement filter = driver.findElement(COMIC_BOOK_FILTER_XPATH);
String attribute = filter.getAttribute("aria-checked");
return Boolean.parseBoolean(ariaChecked);
}
public void checkComicBookFilter() {
WebElement filter = driver.findElement(COMIC_BOOK_FILTER_XPATH);
filter.click();
}
public boolean getEBookFilterState() {
WebElement filter = driver.findElement(E_BOOK_FILTER_XPATH);
String attribute = filter.getAttribute("aria-checked");
return Boolean.parseBoolean(ariaChecked);
}
public void checkEBookFilter() {
WebElement filter = driver.findElement(E_BOOK_FILTER_XPATH);
filter.click();
}
}
The test script changes for better with the new page classes:
@Test
public void canFilterByBooksTest() {
HomePage homePage = new HomePage(driver);
homePage.open();
verify(homePage.isDisplayed(), "home page is not displayed!");
homePage.searchBy(KEYWORD);
ResultsPage resultsPage = new ResultsPage(driver);
verify(resultsPage.isDisplayed(), "results page is not displayed!");
verify(resultsPage.getResultsFound().contains("1 to 10"),
"results found label does not include 1 to 10!");
assertFalse(resultsPage.getBookFilterState(), "checked book filter!");
resultsPage.checkBookFilter();
assertTrue(resultsPage.getBookFilterState(), "unchecked book filter!");
resultsPage.checkBookFilter();
assertFalse(resultsPage.getBookFilterState(), "checked book filter!");
}
The test script starts looking more like English with methods such as open(), searchFor(), checkBookFilter(), get BookFilterState().
It also uses better assertions for verifying the filter state (assertTrue and assertFalse instead of assertEquals).
The test script is shorter.