Skip to content

Latest commit

 

History

History
439 lines (333 loc) · 12.8 KB

README.md

File metadata and controls

439 lines (333 loc) · 12.8 KB

Simplelenium

A simple and robust layer on top of Selenium and PhantomJS.

It also supports Chrome and Firefox. SauceLabs support is ongoing.

Goal

Testing web pages with Selenium can prove difficult. I've seen a lot of projects with an unstable build because of Selenium. To be fair, it's more because of the way Selenium is used. Although experience showed me that using Selenium properly is harder that one might think.

In fact I think that proper usage of Selenium must be left out of tester hands and baked into a small, effective library. Simplelenium is my attempt to do so and it served me well.

Simplelenium deals properly and out-of-the-box with timing issues and StaleElementReferenceExceptions. It supports running tests in parallel without you thinking about it. It doesn't open annoying windows since it's default behaviour is to use PhantomJS, a headless browser.

Give it a try and you'll be surprised at how Selenium testing can be fun again (was it ever?).

Setup (Maven)

Add Simplelenium as a maven test dependency to your project and you are all set to go. Simplelenium requires java 8.

<dependency>
  <groupId>net.code-story</groupId>
  <artifactId>simplelenium</artifactId>
  <version>2.2</version>
  <scope>test</scope>
</dependency>

The first time you run a test, it will download PhantomJS automatically for you so that nothing has to be installed on the machine. mvn clean install is all one should need!

Build status

Build Status

Quick Start

import net.codestory.simplelenium.SeleniumTest;
import org.junit.Test;

public class QuickStartTest extends SeleniumTest {
  @Test
  public void web_driver_site() {
    goTo("http://docs.seleniumhq.org/projects/webdriver/");

    find("#q").fill("StaleElementReferenceExceptions");
    find("#submit").click();

    find("a.gs-title")
      .should()
      .haveMoreItemsThan(5)
      .contain("Issue 1887 - selenium - Element not found in the cache")
      .not().contain("Selenium Rocks!");
  }
}

Notice the fluent api that doesn't rely on static imports. This will make your life easier.

Lots of finders, actions and verifications are supported. Notice that no timing information is provided. The default settings should be ok the vast majority of times.

Finders

Finding elements start with either a find("cssSelector") or a find(org.openqa.selenium.By) . There's no other choice. That's simple. You can use the full power of cssSelector, which should be enough most of the time, or use standard Selenium org.openqa.selenium.By sub-classes.

Searching is not done until a verification is made on the elements. Simplelenium is both lazy and tolerant to slow pages and ongoing refreshes. You don't have to worry about it. Just write what the page should look like and if it happens within a sound period of time, the next verification is made.

We'll dig into more details in the last section.

Verifications

The most simple verification is to check that elements are found:

find(".name").should().exist();

Of course more complex verifications are supported:

find(".name").should().contain("a word", "anything");
find(".name").should().match(Pattern.compile("regexp"));
find(".name").should().beEmpty();
find(".name").should().beEnabled();
find(".name").should().beDisplayed();
find(".name").should().beSelected();
find(".name").should().haveMoreItemsThan(min);
find(".name").should().haveSize(10);
find(".name").should().haveLessItemsThan(max);
find(".name").should().haveDimension(width, height);
find(".name").should().beAtLocation(x, y);

Verifications can be inverted:

find(".name").should().not().contain("a word");

Verifications can be chained:

find(".name")
  .should()
  .contain("a word")
  .contain("anything")
  .beSelected()
  .not().beDisplayed();

The way Simplelenium deals with timing issue is basic, yet efficient:

  • It tries to make the search
  • Then the verification
  • If it passes, then we're cool
  • If not, it tries again immediately with a new search to avoid Staled elements
  • It does so for at least 5 seconds

The "magic" comes from:

  • Not searching until you need to check something
  • Searching again if the check fails
  • Doing a lot of retries as quickly as possible
  • Using the fact that you tell what the page should look like and consider all the failures as false negatives. That is until the maximum delay is reached.

Default timeout can be set using this syntax:

find(".name").should().within(1, MINUTE).contain("a word");

Narrowing search

Sometimes, searching elements is more difficult than using a simple css selector. Simplelenium supports narrowing searches with additional filters, like those:

find("...").withText().beingEmpty().should()...;
find("...").withText().containing("text").should()...;
find("...").withName().startingWith("text").should()...;
find("...").withId().equalTo("text").should()...;
find("...").with("name").matching(Pattern.compile(".*value")).should()...;
find("...").withTagName().equalTo("h1").should()...;
find("...").withClass().containingWord("blue").should()...;
find("...").withCss("color").not().endingWith("grey").should()...;
find("...").withText().startsWith("Prefix").endingWith("Suffix").should()...;
...

Also multiple results can be filtered out this way:

find("...").first();
find("...").second();
find("...").third();
find("...").fourth();
find("...").nth(5);
find("...").limit(10);
find("...").skip(3);
find("...").skip(5).limit(20);
find("...").last();

Actions

Often, you have to interact with the page, not just make verifications. Simplelenium supports a lot of actions. Here are some of them:

find("...").fill("name");
find("...").submit();
find("...").click();
find("...").click(x, y);
find("...").pressReturn();
find("...").sendKeys("A", "B", "C");
find("...").clear();
find("...").doubleClick();
find("...").clickAndHold();
find("...").contextClick();
find("...").release();

find("...").select("text");
find("...").deselect();
find("...").deselectByValue("value");
find("...").deselectByVisibleText("text");
find("...").deselectByIndex(index);
find("...").selectByIndex(index);
find("...").selectByValue("value");

If that's not enough, three generic methods give you access to the Selenium Api underneath but in a managed fashion:

To do anything with the underlying WebElement:

find("...").execute(Consumer<? super WebElement> action);

To execute actions on the element:

find("...").executeActions(String description, BiConsumer<WebElement, Actions> actionsOnElement);

To execute selections on the element:

find("...").executeSelect(String description, Consumer<Select> selectOnElement);

Those three methods should hopefully not be used often but it's great to know that the full power of Selenium is there underneath.

Advanced topics

Let's say you are not impressed, what else can Simplelenium do to make writing tests easier?

Page Objects and Section Objects

Using Page Objects and Section Objects, one can encapsulate both the extraction of web elements and the verification, in a more domain oriented fashion. This also removes a lot of boilerplate code and decreases code duplication.

Let's take a look at a small example:

import net.codestory.simplelenium.DomElement;
import net.codestory.simplelenium.PageObject;
import net.codestory.simplelenium.SeleniumTest;
import org.junit.Test;

public class QuickStartTest extends SeleniumTest {
  Home home;

  @Override
  protected String getDefaultBaseUrl() {
    return "http://localhost:8080/base/";
  }

  @Test
  public void check_page() {
    goTo(home);

    home.shouldDisplayHello();
    home.shouldLinkToOtherPages();
  }

  static class Home implements PageObject {
    DomElement title;
    DomElement greeting;
    DomElement links = find("a.sections");

    @Override
    public String url() {
      return "/home";
    }

    void shouldDisplayHello() {
      title.should().contain("Home page");
      greeting.should().contain("Hello");
    }

    void shouldLinkToOtherPages() {
      links.should().haveSize(5).and().contain("Section1", "Section5");
    }
  }
}

How cool is that? All you have to do is implement PageObject. Page Objects are automatically injected into tests. So are DomElements present as fields into Page Objects. By default elements a searched by name or id but one can use standard find(...) methods to override this behaviour. Same as usual.

If you make the additional effort to return this in Page Objects methods, you than have a nice fluent api.

@Test
public void check_page() {
  home
    .goTo()
    .shouldDisplayHello()
    .shouldLinkToOtherPages();
}

static class Home implements PageObject {
  @Override
  public String url() {
    return "/home";
  }

   Home goTo() {
    goTo(url());
    return this;
  }

   Home shouldDisplayHello() {
    ...
    return this;
  }

   Home shouldLinkToOtherPages() {
    ...
    return this;
  }
}

Page Objects represent a Page with a url. For sections of pages, you can implement SectionObject instead. It makes it easy to split a page into multiple reusable parts that carry their own finders and verifications.

Sections are injected automatically into tests, page objects and other sections.

Running tests in parallel

Simplelenium is good at running tests in parallel. In fact without you doing anything on the code side, it should just work.

Simplelenium keeps a distinct WebDriver for each thread. You don't have to think about it. Let's say you configure surefire to run tests in parallel at class or method level. Easy! You don't have to copy this configuration, with a different syntax, into your test framework. It will just work.

Running tests in parallel with multiple JVMs also works well. We use a lock on the filesystem when we download PhantomJS. I told you, you don't have to think about it.

Tests without JUnit

Sometimes, running the tests with JUnit is not what you want. You'd like to do your own threading and own test lifecycle. You can then use the FluentTest class:

import org.junit.Test;

import static java.util.stream.IntStream.range;

public class FluentTestTest {
  @Test
  public void parallel() {
    String baseUrl = ...;

    range(0, 20).parallel().forEach(index -> {
      new FluentTest(baseUrl)
        .goTo("/")
        .find("h1").should().contain("Hello World").and().not().contain("Unknown")
        .find("h2").should().contain("SubTitle")
        .find(".age").should().contain("42")
        .goTo("/list")
        .find("li").should().contain("Bob").and().contain("Joe");
    });
  }
}

How cool is that?

Run tests with Chrome or Firefox

Even if Simplelenium supports PhantomJs out of the box and by default, tests can be run on Chrome or Firefox.

Run tests with '-Dbrowser=chrome', '-Dbrowser=firefox', '-Dbrowser=phantom_js' to choose which browser you want to use.

If you choose chrome, Simplelenium will download chromedriver automatically for you.

If you don't choose phantomjs, then you have to manually install Firefox or Chrome. Make sure you install them where FirefoxDriver and ChromeDriver expect them to be. If you used homewbrew to install Chrome, like I do, ChromeDriver will figure this out.

If you need to set the port used by ChromeDriver, use the chromedriver.port system property. The default is to use a random free port.

Custom Download URL

If you can't access default download url from where you are (thank you corporate IT proxy). You can override them by providing the following System properties :

What Simplelenium doesn't do

Reading properties from web elements

Sometimes, you want to read a property of a web element and use your own assertions framework to verify if it's ok. That's not how Simplelenium works. You should be able to expect what the element will look like and tell Simplelenium to check. Otherwise you might extract a value a bit too soon and there you are, back into timing hell, with false negative tests. You don't want that. Trust me.

Here's the Simplelenium way of doing this:

find("...").should().match(element -> /* Test something on every element found /*);

Release

mvn release:clean release:prepare release:perform