To atdd-and-beyond

Preview:

Citation preview

John Ferguson Smart

To ATDD And BeyondBetter Automated Acceptance Testing on the JVM

John Ferguson Smart

ConsultantTrainerMentorAuthorSpeakerCoder

The Plan

1) What is ATDD?

2) How can I do it

3) Let’s see it work

The story of your app

Acceptance Test Driven Development

Features/Epics

Make money by selling our stuff online

Display catalog

Goals

Order products

Build a wishlist

Browse CatalogIn order to find stuff I would like to buyAs a customerI want to be able to browse through the catalog

User stories

Browse CatalogIn order to find stuff I would like to buyAs a customerI want to be able to browse through the catalog

User stories

☑  See  all  the  top-­‐level  categories  in  the  catalog☑  Browse  through  the  sub-­‐categories☑  See  all  the  products  in  a  category

Acceptance criteria

Browse CatalogIn order to find stuff I would like to buyAs a customerI want to be able to browse through the catalog

User stories

☑  See  all  the  top-­‐level  categories  in  the  catalog☑  Browse  through  the  sub-­‐categories☑  See  all  the  products  in  a  category

Acceptance criteria

Scenario: See all top-level categoriesGiven I want to browse the catalogWhen I am on the home pageThen I should see the following product categories: Clothing, Accessories, Shoes

Automated acceptance test

Implemented development tests Implemented acceptance tests

Automated acceptance test

Automated acceptance criteria

Define your goals

Automated acceptance criteria

keep you on track

Automated acceptance criteria

Provide better visibility

Automated acceptance criteria

Allow faster release cycles

Automated acceptance criteria

Reduce Risk

Automated acceptance criteria

Provide more value

Delivery  Time

Trad

i4on

al

Using  ATD

D

31%  faster  delivery

Automated acceptance criteria

Higher Quality

Defect  R

ate

Trad

i4on

al

Using  ATD

D

4  4mes  less  defects

Why only do QA at the end of the project?

Real quality cannot be injected at the end

It must be part of the process

Leave the boring stuff to the automated tests...

Let testers focus on more intelligent testing

...and empower your QA team

20

Keeping an eye on things

21

(Think “Two-CDs”)

22

2 Automate your acceptance criteria

1 Discover your acceptance criteria

4 Execute your acceptance tests

3 Implement your acceptance criteria

23

1 Discover your acceptance criteria

Feature: Browse CatalogIn order to find items that I would like to buyAs a customerI want to be able to browse through the catalog

Story: Browse by categoryIn order to find items more easilyAs a customerI want to be able to browse through the product categories

Acceptance CriteriaSee all the top-level categoriesBrowse through the category hierarchyShould display the correct products for each categoryEach category should have the correct sub-categories

Define acceptance criteria for each story

24

1 Discover your acceptance criteria

Acceptance CriteriaSee all the top-level categoriesBrowse through the category hierarchyShould display the correct products for each categoryEach category should have the correct sub-categories

Scenario: See all top-level categoriesGiven I want to browse the catalogWhen I am on the home pageThen I should see the following product categories: Clothing, Accessories, Shoes

Clarify the acceptance criteria with examples

25

2 Automate your acceptance criteria

Story: Browse by categoryIn order to find items more easilyAs a customerI want to be able to browse through the product categories

Acceptance CriteriaSee all the top-level categoriesBrowse through the category hierarchyShould display the correct products for each categoryEach category should have the correct sub-categories

Scenario: See all top-level categoriesGiven I want to browse the catalogWhen I am on the home pageThen I should see the following product categories: Clothing, Accessories, Shoes

Narrative:In order to find items more easilyAs a customerI want to be able to see what product categories exist

Scenario: See all top-level categoriesGiven I want to browse the catalogWhen I am on the home pageThen I should see the following product categories: Clothing, Accessories, Shoes

We now have an executable requirement

26

2 Automate your acceptance criteria

...but they will be reported as ‘pending’

27

3 Implement your acceptance criteria

Narrative:In order to find items more easilyAs a customerI want to be able to see what product categories exist

Scenario: See all top-level categoriesGiven I want to browse the catalogWhen I am on the home pageThen I should see the following product categories: Clothing, Accessories, Shoes

28

3 Implement your acceptance criteria

29

3 Implement your acceptance criteria

JUnit

30

4 Execute your acceptance tests

31

32

33

34

35

36

Functional Test Coverage

“What was tested”

vs

“What’s been done”

What have we tested?

Broken

Works

In Progress

What have we finished?

Finished

Specified but no tests

Partially done?

What have we finished?

What have we finished?

What have we finished?

Better organised requirements

Stay on top of your scenarios

Stay on top of your scenarios

Stay on top of your scenarios

Better visibility on what is doneMore targeted reporting

CapabilityIn order to increase the number of items I sellAs a sellerI want buyers to be able to view ads for items they might want to purchase

FeatureIn order to increase sales of advertised articlesAs a sellerI want potential buyers to be able to display only the ads for articles that they might be interested in purchasing.

StoryIn order to find the items I am interested in fasterAs a buyerI want to be able to list all the ads with a particular keyword in the description or title.

Goal: In order to increase revenue from commissions on classified ads salesAs the head of the classified ads departmentI want to increase the number of items sold via our classified ads

Keep your scenarios organized

Store your requirements where it suits you

RequirementsTagProvidergetRequirements()getParentRequirementOf(testOutcome)

MyRequirementProvider

Meta:@issue MYPROJ-123

Scenario: Search by keywordGiven that I want to find products in the "<range>" rangeWhen I search for products by keyword "<keywords>"Then I should see a product with the title <expectedTitle>

CapabilityIn order to increase the number of items I sellAs a sellerI want buyers to be able to view ads for items they might want to

FeatureIn order to increase sales of advertised articlesAs a sellerI want potential buyers to be able to display only the ads for articles

MYPROJ-123StoryIn order to find the items I am interested in fasterAs a buyerI want to be able to list all the ads with a particular keyword in the description or title.

Customize Thucydides to work for you

• Non-functional requirements• Iterations• Current iteration vs regression tests• Acceptance tests vs more detailed tests• Related features• System components• ...

Use tags for orthogonal concerns

Use tags for orthogonal concerns

@RunWith(ThucydidesRunner)@Issue("NC-1")@WithTag(name="Browse Ads")class BrowseAdsByCategory {    @Managed    public WebDriver webdriver

    @ManagedPages(defaultUrl = "http://www.newsclassifieds-uat.appspot.com")    public Pages pages

    @Steps    BuyerSteps buyer

    @Test    void "Browse ads by category"() {        buyer.opens_home_page()        buyer.chooses_classification "Furniture & Homewares"        buyer.should_see_search_results_for "Furniture & Homewares"

        buyer.chooses_entry(1).from().other_results()        buyer.should_see_details_for_the_selected_ad()

        buyer.returns_to_search_results()        buyer.should_see_search_results_for "Furniture & Homewares"    }}

Tagging in JUnit

Use tags for orthogonal concernsMeta:@tag component:search

Scenario: Search by keywordGiven that I want to find products in the "<range>" rangeWhen I search for products by keyword "<keywords>"Then I should see a product with the title <expectedTitle>

Examples:| range | keywords | expectedTitle || Simone | Simone | 3by3 Simone || Grey Steel Classic | Steel | 3by3 Grey Steel Classic || KV Bags | KV | KV Classic Satchel |

Tagging in JBehave

Use tags for orthogonal concernsMeta:@tag component:search

Scenario: Search by keywordGiven that I want to find products in the "<range>" rangeWhen I search for products by keyword "<keywords>"Then I should see a product with the title <expectedTitle>

Examples:| range | keywords | expectedTitle || Simone | Simone | 3by3 Simone || Grey Steel Classic | Steel | 3by3 Grey Steel Classic || KV Bags | KV | KV Classic Satchel |

More structured tests

Have you got web tests that look like this?

Have you got web tests that look like this?

Page Objects To The Rescue!

Page Objects To The Rescue!

Better encapsulationEasier to readEasier to maintainStill a bit unclear what we are doing

Test Steps To The Rescue!

Step methods focus on what we are doing

Test Steps To The Rescue!

Step implementations detail the how

    @Step("The carousel should display {0} at a time")    def should_see_a_number_of_visible_ads_in_the_carousel(int adCount) {        assert homePage.visibleCarouselAds.size() == adCount    }

Test Steps To The Rescue!

    @Step    public void proceedToPayment() {        adPreviewPage.proceedToPayment();    }

Steps are ordinary methods

   @Step   public void searches_for(String searchTerms) {        homePage.searchFor(searchTerms);   }

They can have parameters

   @Pending @Step    public void searches_for(String searchTerms) {        // Not done yet    }

They can be pending

You can customize the title

Test Steps To The Rescue!

    @Step    def browses_product_categories(String... categories) {        selects_top_level_category(categories.head())        categories.tail().each { subcategory ->            browse_through_subcategory(subcategory)        }    }

    @Step    def selects_top_level_category(String category) {        homePage.select_top_level_category category    }        @Step    def browse_through_subcategory(subcategory) {        homePage.select_subcategory(subcategory)    }

Steps can call other steps

Test Steps To The Rescue!

Test reports document the what and the how

Test Steps To The Rescue!

Focus on what we are doing, not how we do itMore reusabilityBetter reporting

Even better with JBehave!

Narrative:In order to find items more easilyAs a customerI want to be able to browse through the product categories

Scenario: Browse through product categoriesGiven I am on the home pageWhen I browse through the product categories Clothing, Mens, ShirtsThen I should see a product with the title 3by3 Milano Stretch Cotton Shirt

JBehave describes what we are doing

Use normal Thucydides steps in the implementation

Even better with JBehave!

Narrative:In order to find items more easilyAs a customerI want to be able to browse through the product categories

Scenario: Browse through product categoriesGiven I am on the home pageWhen I browse through the product categories Clothing, Mens, ShirtsThen I should see a product with the title 3by3 Milano Stretch Cotton Shirtclass BrowseByCategorySteps extends BigCommerceJBehaveSteps{

    @Steps    CustomerSteps customer;

    @Given('I am on the home page')    public void givenIAmOnTheHomePage() {        customer.opens_home_page()    }        @When('I browse through the product categories $categories')    public void whenIBrowseThroughTheProductCategories(List<String> categories) {        customer.browses_product_categories(*categories)    }

    @Then('I should see a product with the title "$expectedTitle"')    public void thenIShouldSeeAProductWithTheTitle(String expectedTitle) {        customer.should_see_product withTitle(expectedTitle)    }}

Use normal Thucydides steps

Even better with JBehave!

Narrative:In order to find items more easilyAs a customerI want to be able to browse through the product categories

Scenario: Browse through product categoriesGiven I am on the home pageWhen I browse through the product categories Clothing, Mens, ShirtsThen I should see a product with the title 3by3 Milano Stretch Cotton Shirt

Context

What

How

Illustrations

Even better with JBehave!

Narrative:In order to find items more easilyAs a customerI want to be able to browse through the product categories

Scenario: Browse through product categoriesGiven I am on the home pageWhen I browse through the product categories Clothing, Mens, ShirtsThen I should see a product with the title 3by3 Milano Stretch Cotton Shirt

Living documentationEasier to readEasier to maintainMore work maintaining the .story files

Better Page Objects

Fluent selectorsFluent matchersHTML tablesFluent waits

Thucydides Page Object support

Thucydides Fluent Selectorspublic class HomePage extends PageObject {

    @FindBy(name="adFilter.searchTerm")    WebElement searchTerm;

    @FindBy(css=".keywords button")    WebElement search;

    public HomePage2(WebDriver driver) {        super(driver);    }

    public void chooseCategory(String category) {        findBy("#category-select").then(".arrow").then().click();        findBy("#category-select").then(By.linkText(category)).then().click();    }

    public void enterKeywords(String keywords) {        $(searchTerm).type(keywords);    }

    public void performSearch() {        $(search).click();    }}

Fluent selectors

Thucydides helper methods

Thucydides Fluent Selectorsclass ShoppingCartPage extends PageObject {

    ShoppingCartPage(WebDriver driver) {        super(driver)    }

    @FindBy(css = ".CartContents tbody tr")    List<WebElement> shoppingCartItems

    List<CartItem> getCartItems() {         shoppingCartItems.collect { cartItemfromElement(it) }     }

    CartItem cartItemfromElement(WebElement element) {        Integer quantity = Integer.parseInt(itemQuantity(element))        String product = productName(element)        BigDecimal itemPrice = priceOf(itemPrice(element))        BigDecimal totalPrice = priceOf(totalPrice(element))

        return CartItem.containing(quantity).productsCalled(product).                                             withAnItemPriceOf(itemPrice).                                             andATotalOf(totalPrice)    } ...

Page Object returns domain classes

Building a CartItem from the WebElement

Thucydides Fluent Selectorsclass ShoppingCartPage extends PageObject {

    ShoppingCartPage(WebDriver driver) {        super(driver)    }

    @FindBy(css = ".CartContents tbody tr")    List<WebElement> shoppingCartItems

    List<CartItem> getCartItems() {         shoppingCartItems.collect { cartItemfromElement(it) }     }

    CartItem cartItemfromElement(WebElement element) {        Integer quantity = Integer.parseInt(itemQuantity(element))        String product = productName(element)        BigDecimal itemPrice = priceOf(itemPrice(element))        BigDecimal totalPrice = priceOf(totalPrice(element))

        return CartItem.containing(quantity).productsCalled(product).                                             withAnItemPriceOf(itemPrice).                                             andATotalOf(totalPrice)    } ...

    private String productName(WebElement cartItem) {        cartItem.findElement(By.className("ProductName")).text    }

    private String totalPrice(WebElement cartItem) {        cartItem.findElement(By.className("CartItemTotalPrice")).text    }

    private String itemPrice(WebElement cartItem) {        cartItem.findElement(By.className("CartItemIndividualPrice")).text    }

    private String itemQuantity(WebElement cartItem) {        def itemQuantityDropdown           = cartItem.findElement(                     By.xpath(".//td[contains(@class,'CartItemQuantity')]/select"))        element(itemQuantityDropdown).selectedValue    }

Classic WebDriver

Thucydides Fluent Selectorsclass ShoppingCartPage extends PageObject {

    ShoppingCartPage(WebDriver driver) {        super(driver)    }

    @FindBy(css = ".CartContents tbody tr")    List<WebElement> shoppingCartItems

    List<CartItem> getCartItems() {         shoppingCartItems.collect { cartItemfromElement(it) }     }

    CartItem cartItemfromElement(WebElement element) {        Integer quantity = Integer.parseInt(itemQuantity(element))        String product = productName(element)        BigDecimal itemPrice = priceOf(itemPrice(element))        BigDecimal totalPrice = priceOf(totalPrice(element))

        return CartItem.containing(quantity).productsCalled(product).                                             withAnItemPriceOf(itemPrice).                                             andATotalOf(totalPrice)    } ...

    private String productName(WebElement cartItem) {        element(cartItem).findBy(".ProductName").text    }

    private String totalPrice(WebElement cartItem) {        element(cartItem).findBy(".CartItemTotalPrice").text    }

    private String itemPrice(WebElement cartItem) {        element(cartItem).findBy(".CartItemIndividualPrice").text    }

    private String itemQuantity(WebElement cartItem) {        element(cartItem).findBy(".CartItemQuantity").then("select").selectedValue    }

Using Thucydides Fluent Selectors

Thucydides Fluent Selectorsclass ShoppingCartPage extends PageObject {

    ShoppingCartPage(WebDriver driver) {        super(driver)    }

    @FindBy(css = ".CartContents tbody tr")    List<WebElement> shoppingCartItems

    List<CartItem> getCartItems() {         shoppingCartItems.collect { cartItemfromElement(it) }     }

    CartItem cartItemfromElement(WebElement element) {        Integer quantity = Integer.parseInt(itemQuantity(element))        String product = productName(element)        BigDecimal itemPrice = priceOf(itemPrice(element))        BigDecimal totalPrice = priceOf(totalPrice(element))

        return CartItem.containing(quantity).productsCalled(product).                                             withAnItemPriceOf(itemPrice).                                             andATotalOf(totalPrice)    } ...

    private String productName(WebElement cartItem) {        $(cartItem).findBy(".ProductName").text    }

    private String totalPrice(WebElement cartItem) {        $(cartItem).findBy(".CartItemTotalPrice").text    }

    private String itemPrice(WebElement cartItem) {        $(cartItem).findBy(".CartItemIndividualPrice").text    }

    private String itemQuantity(WebElement cartItem) {        $(cartItem).findBy(".CartItemQuantity").then("select").selectedValue    }

Using short-hand Thucydides Fluent Selectors

Thucydides Fluent Selectorsclass ShoppingCartPage extends PageObject {

    ShoppingCartPage(WebDriver driver) {        super(driver)    }

    @FindBy(css = ".CartContents tbody tr")    List<WebElement> shoppingCartItems

    List<CartItem> getCartItems() {         shoppingCartItems.collect { cartItemfromElement(it) }     }

    CartItem cartItemfromElement(WebElement element) {        Integer quantity = Integer.parseInt(itemQuantity(element))        String product = productName(element)        BigDecimal itemPrice = priceOf(itemPrice(element))        BigDecimal totalPrice = priceOf(totalPrice(element))

        return CartItem.containing(quantity).productsCalled(product).                                             withAnItemPriceOf(itemPrice).                                             andATotalOf(totalPrice)    } ...

    private String productName(WebElement cartItem) {        return $(cartItem).findBy(".ProductName").getText();    }

    private String totalPrice(WebElement cartItem) {        return $(cartItem).findBy(".CartItemTotalPrice").getText();    }

    private String itemPrice(WebElement cartItem) {        return $(cartItem).findBy(".CartItemIndividualPrice").getText();    }

    private String itemQuantity(WebElement cartItem) {        return $(cartItem).findBy(".CartItemQuantity").then("select").getSelectedValue();    }

(Pure Java version)

Thucydides Fluent Selectors    List<String> getYAxes() {        def axesElements = findAll(".dygraph-axis-label-y");        axesElements.collect { WebElement axis -> axis.text }    }

Finding multiple elements

Thucydides Fluent Matchers

Thucydides Fluent Matchersimport static org.hamcrest.Matchers.is;import static net.thucydides.core.matchers.BeanMatchers.the;...    @Steps    public DeveloperSteps developer;

    @Test    public void should_search_for_artifacts_by_name() {        developer.opens_the_search_page();        developer.searches_for("Thucydides");        developer.should_see_artifacts_where(the("ArtifactId", is("thucydides")),                                             the("GroupId", is("net.thucydides")));    }

Using Thucydides matchers

And Hamcrest matchers

Thucydides Fluent Matchersimport static org.hamcrest.Matchers.is;import static net.thucydides.core.matchers.BeanMatchers.the;...    @Steps    public DeveloperSteps developer;

    @Test    public void should_search_for_artifacts_by_name() {        developer.opens_the_search_page();        developer.searches_for("Thucydides");        developer.should_see_artifacts_where(the("ArtifactId", is("thucydides")),                                             the("GroupId", is("net.thucydides")));    }

import static net.thucydides.core.matchers.BeanMatcherAsserts.shouldMatch;

public class DeveloperSteps extends ScenarioSteps {

    SearchPage searchPage;    SearchResultsPage searchResultsPage;

    public DeveloperSteps(Pages pages) {        super(pages);        searchResultsPage = pages.getPage(SearchResultsPage.class); searchPage = pages.getPage(SearchPage.class);    }

    @Step    public void opens_the_search_page() {        searchPage.open();    }

    @Step    public void searches_for(String search_terms) {        searchPage.enter_search_terms(search_terms);        searchPage.starts_search();    }

    @Step    public void should_see_artifacts_where(BeanMatcher... matchers) {        shouldMatch(searchResultsPage.getSearchResults(), matchers);    }}

Applying the matchers

The Page Object returns a list of POJOs

Thucydides Fluent Matchersimport static org.hamcrest.Matchers.is;import static net.thucydides.core.matchers.BeanMatchers.the;...    @Steps    public DeveloperSteps developer;

    @Test    public void should_search_for_artifacts_by_name() {        developer.opens_the_search_page();        developer.searches_for("Thucydides");        developer.should_see_artifacts_where(the("ArtifactId", is("thucydides")),                                             the("GroupId", is("net.thucydides")));    }

    public List<Artifact> getResults() {        List<WebElement> rows = resultTable.findElements(By.xpath(".//tr[td]"));        return convert(rows, toArtifacts());    }

    private Converter<WebElement, Artifact> toArtifacts() {        return new Converter<WebElement, Artifact>() {            public Artifact convert(WebElement row) {                List<WebElement> cells = row.findElements(By.tagName("td"));                String groupId = cells.get(0).getText();                String artifactId = cells.get(1).getText();                String latestVersion = cells.get(2).getText();                return new Artifact(groupId, artifactId, latestVersion);            }        };    }

The page object should return POJOs

Thucydides Fluent MatchersList<Person> persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor"));

shouldMatch(persons, the("firstName", is(not("Tim"))));shouldMatch(persons, the("firstName", startsWith("B")));

Matcher work with simple POJOs

Thucydides and Tablesimport static net.thucydides.core.pages.components.HtmlTable.rowsFrom;

public class SearchResultsPage extends PageObject {

    WebElement resultTable;

    public SearchResultsPage(WebDriver driver) {        super(driver);    }

    public List<Map<String, String>> getSearchResults() {        return rowsFrom(resultTable);    }}

Convenience methods: convert a table to a map of Strings

Thucydides and Tables    @Test    public void clicking_on_artifact_should_display_details_page() {        developer.opens_the_search_page();        developer.searches_for("Thucydides");        developer.open_artifact_where(the("ArtifactId", is("thucydides")),                                      the("GroupId", is("net.thucydides")));

        developer.should_see_artifact_details_where(the("artifactId", is("thucydides")),                                                    the("groupId", is("net.thucydides")));    }

Using matchers to click on a row in a table

Thucydides and Tables    @Test    public void clicking_on_artifact_should_display_details_page() {        developer.opens_the_search_page();        developer.searches_for("Thucydides");        developer.open_artifact_where(the("ArtifactId", is("thucydides")),                                      the("GroupId", is("net.thucydides")));

        developer.should_see_artifact_details_where(the("artifactId", is("thucydides")),                                                    the("groupId", is("net.thucydides")));    } @Step    public void open_artifact_where(BeanMatcher... matchers) {        searchResultsPage.clickOnFirstRowMatching(matchers);    }

import static net.thucydides.core.pages.components.HtmlTable.filterRows;...    @FindBy(css="#resultTable")    WebElement resultTable;

    public void clickOnFirstRowMatching(BeanMatcher... matchers) {        List<WebElement> matchingRows = filterRows(resultTable, matchers);        WebElement targetRow = matchingRows.get(0);        WebElement detailsLink = $(targetRow).findBy(".//a[contains(@href,'artifactdetails')]");        detailsLink.click();    }

Pass through matchers

Return matching rows

Thucydides Fluent Waits

Convenience methodsExtends the WebDriver Wait API

Thucydides Fluent Waits    void addToCart(Integer quantity) {        $(quantitySelection).selectByVisibleText(quantity.toString())        $(addToCart).click()        waitForTextToAppear("added to your cart")    }

A simple wait

    void uploadExistingImage(String imageUrl) {        // upload an image        ...        waitForPresenceOf(".photo")    }

Wait for an element to be rendered

    def waitForPageToLoad() {        waitFor(500).milliseconds()        waitForAbsenceOf("#loading")    }

Wait for an element to disappear

Thucydides Fluent Waits    def searchByLocation(postcode) {        element(location).typeAndTab(postcode)        element(locationGo).click()        waitFor("//div[@class='f-item'][contains(.,'${postcode}')]")    }

Waiting for an XPath expression

    def filterByLocation(String state) {        element(locationList).click()        waitFor("#location-select li a")        findBy("//span[@id='location-select']//a[contains(.,'$state')]").then().click()        waitForCondition().until(stateIsChosen(state))

    }

    Function<? super WebDriver, Boolean> stateIsChosen(final String state) {        return new Function<? super WebDriver, Boolean>() {

            @Override            Boolean apply(driver) {                return state == getCurrentSelectedState()            };        }    }

Wait for a non-trivial condition

What are we waiting for

Thucydides Assertsdef shouldDisplaySubcategory(subCategory) {    findBy(".SubCategoryList").then(By.linkText(subCategory)).shouldBeCurrentlyVisible()}

Check that this element is visible

@FindBy(id=”search”)WebElement searchButton

def searchShouldBeEnabled() {    searchButton.shouldBeCurrentlyEnabled()}

Check that this element is enabled

Smarter Webdriver coding

Smarter Webdriver coding

Prefer CSS to XPath

Smarter Webdriver coding

Use Nested Finds

Nested Findspublic void chooseCategory(String category) {    findBy("//div[@id='category-select']//a[contains(@class,'arrow')]").click();    findBy("//div[@id='category-select']//a[.,contains($category)]").click();}

public void chooseCategory(String category) {    findBy("#category-select").then(".arrow").then().click();    findBy("#category-select").then(By.linkText(category)).then().click();}

Hard to read, hard to maintain

A more readable approach

Smarter Webdriver coding

Confine Webdriver to your Page Objects

Encapsulate Page Objectsclass ShoppingCartPage extends PageObject {

    public ShoppingCartPage(WebDriver driver) {        super(driver)    }

    public List<CartItem> getCartItems() {         shoppingCartItems.collect { cartItemfromElement(it) }     }

    @FindBy(css = ".CartContents tbody tr")    private List<WebElement> shoppingCartItems

    private CartItem cartItemfromElement(WebElement element) {        Integer quantity = Integer.parseInt(itemQuantity(element))        String product = productName(element)        BigDecimal itemPrice = priceOf(itemPrice(element))        BigDecimal totalPrice = priceOf(totalPrice(element))

        return CartItem.containing(quantity).productsCalled(product).                                             withAnItemPriceOf(itemPrice).                                             andATotalOf(totalPrice)    } ...

Public API

Webdriver stays private

Smarter Webdriver coding

Webdriver Query = JDBC Query~

John Ferguson Smart

Thank You

To ATDD And BeyondBetter Automated Acceptance Testing on the JVM