pojosontheweb / selenium-utils

selenium-utils
Apache License 2.0
12 stars 5 forks source link

Takes the misery out of selenium !

CircleCI

Findr is a simple yet very powerful utility class that helps to write tests in a "wait-style", without accessing WebDriverWait directly.

The API is slick, easy to use and helps to be DRY and concise. It's based on chained methods and function composition, in order to let you create chains of selectors/assertions. Those chains are then evaluated atomically inside a WebDriverWait, under the hood.

Evaluation succeeds if the whole chain passes. Otherwise, if one element in the chain fails, then the whole chain fails and is retried from the top, until timeout.

Simple example over Google search :

// get google
driver.get("http://www.google.com");

final Findr findr = new Findr(driver);

// perform the search
findr           
    .$("#gbqfq")            
    .sendKeys("pojos on the web", Keys.ENTER); 

// check the results
findr           
    .$("#ires")
    .$$("h3.r")
    .at(0)      
    .$("a")
    .where(textEquals("POJOs on the Web!: Woko"))
    .eval();    

Retrying all

Findr will retry all elements in the chain, until the whole chain passes, or timeouts.

Let's have a look at this simple click :

new Findr(webDriver).$(".foo").$(".bar").click();

In practice, this will create an atomic chain with those steps :

  1. find a WebElement with class foo
  2. find a WebElement with class bar, under the one found in 1.
  3. call click() on the WebElement found in 2.

Again, the chain will be evaluated atomically, so 1, 2 and 3 must pass for the whole expression (the click) to pass. If any step in the chain fails, then it's retried from step 1, until exhaustion.

This is really the heart of the system, what makes tests robust and stable.

Batch / Retry

The library also provides support for composing several "steps" into a single, retry-all operation. This allows to group a set of interactions and make sure that all of them are actually performed.

This can be used as a relacement for "nested" findrs, which are not easy to manage :

// a "nested" findr version that clicks/asserts 
// as an atomic op
void openDropDown() {
    f.$("#my-button").eval( e -> {
        e.click();
        // make sure that dropdown is shown
        // if not then the top-level eval()
        // will be called again
        // we use a "nested" Findr
        f.$("#my-dropdown")                
            .where(isDisplayed())
            .setTimeout(5) // must be < to the parent's timeout...
            .eval();
    });
}

Can be replaced with Retry :

import static com.pojosontheweb.selenium.Retry.retry;

void openDropDown() {
    retry()
        .add(() -> f.$("#my-button").click())
        .add(f.$("#my-dropdown").where(isDisplayed())
        .eval();
}

The add() method accepts several types of arguments (Findr, Runnable, mapping functions etc.), allowing to create a chain of steps. This chain is then evaluated calling the eval() method on it.

Composition

Findr instances are immutable and can be safely shared and reused :

Findr container = new Findr(driver).$("#container");
container.$("#username").sendKeys("john.doe");
container.$("#the-button").click();

This makes it very easy (and safe) to create helper functions or page objects that reuse Findrs. Again, composed Findrs are evaluated as a single, atomic chain.

Built-in predicates

The Findrs class exposes a set of static factory methods that create Predicate<WebElement>s for the recurrent stuff, for example :

Those can be used directly in your findrs :

new Findr(driver)
    .$("div.my-class"))
    .where(attrEquals("my-attr", "my-value"))
    .where(textEquals("This is some content"))
    .eval();

Error reporting

Findr tries to report failures in condition chains by including a String-ified version of the path. Of course, the stack trace of the Timeout exception will tell where the evaluation failed.

There are also variants to eval() that accept a failureMessage argument.

Verbose logging

Findr executes the various functions you compose as a "back box", and it's sometimes hard to understand where it went wrong in the conditions chain. In order to get insights about what's going on, you can set the sys prop webtests.findr.verbose, so that it outputs the logs (to stdout) when asserting the condition chain.

WebDriver init

Use DriverBuilder in order to create instances of WebDriver. The API can be used statically :

// create a simple Chrome Driver
WebDriver driver = DriverBuildr
    .chrome()
    .setDriverPath(new File("/path/to/chromedriver"))
    .build();

Or by defining system properties :

WebDriver = DriverBuilder.fromSysProps().build();

The latter approach allows for more flexible builds.

System Properties

Here is a list of all supported System Properties :

property allowed values default comment
General props
webtests.browser firefox,chrome firefox
webtests.locales en, fr, ... Comma-separated list of locale(s) for the tests (browser language)
webtests.hub.url valid remote driver url Connect to a Selenium Grid (RemoteWebDriver). Video recording is not available when using remote drivers.
webtests.findr.timeout Any (reasonable) positive integer 10 The Findr timeout in seconds
webtests.findr.sleep Any (reasonable) positive long 500 The Findr sleep interval in milliseconds. Allows to control polling frequency.
webtests.findr.verbose true,fase false log some infos about findr evaluation chains (helps debugging)
webtests.video.enabled true,false false enables video recording of failed tests
webtests.video.dir path to folder tmp dir
webtests.video.failures.only true,false true keep videos for failures only, or for all tests
Chrome only
webdriver.chrome.driver path to driver exe mandatory for Chrome

TestCase plumbing

Base classes are included that manage the driver init/close and video stuff. If you use JUnit for example, you simply have to extend a base class :

public class MyTest extends ManagedDriverJunit4TestBase {

    @Test
    public void testMe()  {
        WebDriver d = getWebDriver();
        ...
    }

}

Doing so will allow you to run your test directly, and parameterize it using sys props.

There's also a TestUtil class that implements the lifecycle of a typical test. You can delegate to that one if you already extend a base class in your test.

Video recording

We have a very basic ScreenRecordr class that performs video capture on the host that runs the webdriver. It's activated by the TestCase plumbing, via sys props.

It's built on Monte Media Library, and is pure Java. It's been tested on a different platforms (mac, windows, linux), and even seems to work in headless/xvfb environments.

Using with Maven

Add the dependency to your pom :

<dependency>
    <groupId>com.pojosontheweb</groupId>
    <artifactId>selenium-utils-core</artifactId>
    <version>LATEST-SNAPSHOT</version>
    <scope>test</scope>
</dependency>

Configure surefire :

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <systemPropertyVariables>
            <webtests.browser>${webtests.browser}</webtests.browser>
            <webtests.video.enabled>${webtests.video.enabled}</webtests.video.enabled>
            <webtests.video.dir>${project.build.directory}/webtests-videos</webtests.video.dir>
            <webdriver.chrome.driver>${webdriver.chrome.driver}</webdriver.chrome.driver>
        </systemPropertyVariables>
    </configuration>
</plugin>

Invoke maven :

mvn test

With sys props :

mvn test -Dwebtests.browser=chrome -Dwebdriver.chrome.driver=/opt/chromedriver -Dwebtests.video.enabled=true

Page Objects

Included is a simple yet useful AbstractPageObject class that you can use to create your own page helper libraries.

Groovy

For Groovy users, a set of extensions and additional stuff is available as a separate module. Have a look at Taste for more infos.