07 Feb

Selenide tests – The Maven way

Selenide is a wrapper over Selenium which greatly simplifies its use. For instance, to support an AJAX event in Selenium, the code would be something like this:

FluentWait<By> fluentWait = new FluentWait<By>(By.tagName("TEXTAREA"));
fluentWait.pollingEvery(100, TimeUnit.MILLISECONDS);
fluentWait.withTimeout(1000, TimeUnit.MILLISECONDS);
fluentWait.until(new Predicate<By>() {
    public boolean apply(By by) {
        try {
            return browser.findElement(by).isDisplayed();
        } catch (NoSuchElementException ex) {
            return false;
        }
    }
});
assertEquals("John", browser.findElement(By.tagName("TEXTAREA")).getAttribute("value"));

Whereas with Selenide would be something like:

$("TEXTAREA").shouldHave(value("John"));

and it is basically this way with everything (there are plenty of examples on a dedicated page at Selenide’s GitHub project).

Selenide logo

Moreover, in the functional testing world, there is a well-known pattern, Page Objects, which boils down to create a dedicated class (a Page Object) for each page which is going to be simulated. These Page Object classes would them implement the access to each page element, in order to reduce code duplication throughout the entire test suite. With Selenide, this pattern more or less looks like this:

package com.sweftt.swdm.pages;

import org.openqa.selenium.By;

import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.page;


public class GooglePage {
    
    public SearchResultsPage searchFor( String text ) {
        $( By.name( "q" ) ).val( text ).pressEnter();
        return page( SearchResultsPage.class );
    }
    
}
package com.sweftt.swdm.pages;

import com.codeborne.selenide.ElementsCollection;
import com.codeborne.selenide.SelenideElement;

import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$$;


public class SearchResultsPage {

    public ElementsCollection getResults() {
        return $$( "#ires .g" );
    }

    public SelenideElement getResult( int index ) {
        return $( "#ires .g", index );
    }
    
}
    @Test
    public void userCanSearch() {
        GooglePage page = open( "http://google.com", GooglePage.class );
        SearchResultsPage results = page.searchFor( "selenide" );
        results.getResults().shouldHave( size( 10 ) );
        results.getResult( 0 ).shouldHave( text( "Selenide: concise UI tests in Java" ) );
    }

(by default, the test will use Firefox to perform the navigation, although it can be changed either through environment properties, -Dselenide.browser=ie, or through the com.codeborne.selenide.Configuration class)

The only problem is that, when running this test, on a clean environment, something like this will show up:

org.openqa.selenium.firefox.NotConnectedException: Unable to connect to host 127.0.0.1 on port 7055 after 45000 ms. Firefox console output:
pi	DEBUG	Calling bootstrap method startup on aushelper@mozilla.org version 1.0
1485199741216	addons.xpi	DEBUG	Registering manifest for XXXXXXXXXX\Mozilla Firefox\browser\features\e10srollout@mozilla.org.xpi
1485199741216	addons.xpi	DEBUG	Calling bootstrap method startup on e10srollout@mozilla.org version 1.5
1485199741217	addons.xpi	DEBUG	Registering manifest for XXXXXXXXXX\Mozilla Firefox\browser\features\firefox@getpocket.com.xpi
[~40 lines of stacktrace omitted]
1485199759009	addons.manager	DEBUG	Calling shutdown blocker for LightweightThemeManager
1485199759009	addons.manager	DEBUG	Calling shutdown blocker for GMPProvider
1485199759012	addons.manager	DEBUG	Calling shutdown blocker for PluginProvider
1485199759013	addons.manager	DEBUG	Calling shutdown blocker for <unnamed-provider>
1485199759015	addons.manager	DEBUG	Calling shutdown blocker for PreviousExperimentProvider
1485199759018	addons.xpi	DEBUG	Notifying XPI shutdown observers
1485199759023	addons.manager	DEBUG	Async provider shutdown done

	at org.openqa.selenium.firefox.internal.NewProfileExtensionConnection.start(NewProfileExtensionConnection.java:113)
	at org.openqa.selenium.firefox.FirefoxDriver.startClient(FirefoxDriver.java:347)
	at org.openqa.selenium.remote.RemoteWebDriver.<init>(RemoteWebDriver.java:116)
	at org.openqa.selenium.firefox.FirefoxDriver.<init>(FirefoxDriver.java:259)
	at org.openqa.selenium.firefox.FirefoxDriver.<init>(FirefoxDriver.java:247)
	at org.openqa.selenium.firefox.FirefoxDriver.<init>(FirefoxDriver.java:242)
	at org.openqa.selenium.firefox.FirefoxDriver.<init>(FirefoxDriver.java:135)
	at com.codeborne.selenide.webdriver.WebDriverFactory.createFirefoxDriver(WebDriverFactory.java:123)
	at com.codeborne.selenide.webdriver.WebDriverFactory.createWebDriver(WebDriverFactory.java:47)
	at com.codeborne.selenide.impl.WebDriverThreadLocalContainer.createDriver(WebDriverThreadLocalContainer.java:240)
	at com.codeborne.selenide.impl.WebDriverThreadLocalContainer.getAndCheckWebDriver(WebDriverThreadLocalContainer.java:113)
	at com.codeborne.selenide.WebDriverRunner.getAndCheckWebDriver(WebDriverRunner.java:136)
	at com.codeborne.selenide.impl.Navigator.navigateToAbsoluteUrl(Navigator.java:68)
	at com.codeborne.selenide.impl.Navigator.open(Navigator.java:31)
	at com.codeborne.selenide.Selenide.open(Selenide.java:81)
	at com.codeborne.selenide.Selenide.open(Selenide.java:150)
	at com.codeborne.selenide.Selenide.open(Selenide.java:131)
	at com.sweftt.swdm.GoogleSearchTest.userCanSearch(GoogleSearchTest.java:20)

The underlying cause is that in order to execute these tests (or Selenium based ones, for all that matters), it is mandatory to download a binary which allows the WebDriver to manage the browser. Furthermore, it is also necessary to set the absolute path to these binaries as Java properties:

System.setProperty( "webdriver.ie.driver", "C:/absolute/path/to/binary/IEDriverServer.exe" );
System.setProperty( "webdriver.edge.driver", "C:/absolute/path/to/binary/MicrosoftWebDriver.exe" );
System.setProperty( "phantomjs.binary.path", "/absolute/path/to/binary/phantomjs" );

From a Maven / CD-CI environment execution standpoint, this is problematic, as it makes these tests environment-dependant. Fortunatelly this can be avoided, through an additional library, WebDriverManager; so, lets see how it does integrate with Selenide. The source code is available at GitHub.

The first thing to do is to add the appropiate dependency, which in Maven’s land would mean adding something like this to your pom:

<dependency>
  <groupId>io.github.bonigarcia</groupId>
  <artifactId>webdrivermanager</artifactId>
  <version>1.5.1</version>
</dependency>

Second, in the test itself, setting up the library:

package com.sweftt.swdm;

import static com.codeborne.selenide.CollectionCondition.size;
import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Selenide.open;
import static com.codeborne.selenide.WebDriverRunner.setWebDriver;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import com.sweftt.swdm.pages.GooglePage;
import com.sweftt.swdm.pages.SearchResultsPage;

import io.github.bonigarcia.wdm.ChromeDriverManager;


/**
 * Google search test with Selenide.
 */
public class GoogleSearchTest {
    
    WebDriver driver;
    
    @BeforeClass
    public static void setupClass() {
        ChromeDriverManager.getInstance().setup();
    }
    
    @Before
    public void setupTest() {
        driver = new ChromeDriver();
        setWebDriver( driver );
    }

    // as the driver hasn't been created by Selenide, closing it is mandatory
    @After
    public void teardown() {
        if( driver != null ) {
            driver.quit();
        }
    }

    @Test
    public void userCanSearch() {
        GooglePage page = open( "http://google.com", GooglePage.class );
        SearchResultsPage results = page.searchFor( "selenide" );
        results.getResults().shouldHave( size( 10 ) );
        results.getResult( 0 ).shouldHave( text( "Selenide: concise UI tests in Java" ) );
    }

}

And that’s all it takes to solve the problem. However, this can be done better: there is too much ceremony, a lot of configuration which will get repeated in each test. JUnit rules to the rescue! First of all, an Enum will be defined, to bind the driver’s configuration classes from WebDriverManager with their corresponding Selenium WebDrivers:

package com.sweftt.swdm.rules;


import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.opera.OperaDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;

import io.github.bonigarcia.wdm.BrowserManager;
import io.github.bonigarcia.wdm.ChromeDriverManager;
import io.github.bonigarcia.wdm.EdgeDriverManager;
import io.github.bonigarcia.wdm.FirefoxDriverManager;
import io.github.bonigarcia.wdm.InternetExplorerDriverManager;
import io.github.bonigarcia.wdm.OperaDriverManager;
import io.github.bonigarcia.wdm.PhantomJsDriverManager;

public enum BrowserManagerEnum {
    
    CHROME( ChromeDriverManager.getInstance() ),
    FIREFOX( FirefoxDriverManager.getInstance() ),
    EDGE( EdgeDriverManager.getInstance() ),
    IE( InternetExplorerDriverManager.getInstance() ),
    MARIONETTE( FirefoxDriverManager.getInstance() ),
    OPERA( OperaDriverManager.getInstance() ),
    PHANTOMJS( PhantomJsDriverManager.getInstance() );
    
    private final BrowserManager bm;

    private BrowserManagerEnum( final BrowserManager bm ) {
        this.bm = bm;
    }
    
    public BrowserManager getBrowserManager() {
        return bm;
    }

    public WebDriver getDriver() {
        switch( this ) {
        case CHROME: return new ChromeDriver();
        case FIREFOX: return new FirefoxDriver();
        case EDGE: return new EdgeDriver();
        case IE: return new InternetExplorerDriver();
        case MARIONETTE: return new FirefoxDriver();
        case OPERA: return new OperaDriver();
        case PHANTOMJS: return new PhantomJSDriver();
        default: return null;
        }
    }

}

Done that, all that remains is the JUnit rule itself:

package com.sweftt.swdm.rules;

import static com.codeborne.selenide.WebDriverRunner.setWebDriver;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.openqa.selenium.WebDriver;


public class WebDriverManagerRule implements TestRule {
	
	protected final BrowserManagerEnum browser;
	protected WebDriver driver = null;
	
	/**
	 * Rule constructor.
	 * 
	 * @param browser see {@link BrowserManagerEnum} for available values.
	 */
	public WebDriverManagerRule( BrowserManagerEnum browser ) {
		this.browser = browser;
	}

	/** {@inheritDoc} */
	@Override
	public Statement apply( Statement base, Description description ) {
		return statement( base );
	}
	
	Statement statement( final Statement base ) {
        return new Statement() {
        	
        	/** {@inheritDoc} */
            @Override
            public void evaluate() throws Throwable {
            	if( driver == null ) {
            		beforeClass();
            		base.evaluate();
            		afterClass();
            	} else {
            		before();
                    try {
                        base.evaluate();
                    } finally {
                        after();
                    }
            	}
            }
        };
    }

    protected void beforeClass() throws Throwable {
    	browser.getBrowserManager().setup();
    	driver = browser.getDriver();
    }

    protected void before() throws Throwable {
    	setWebDriver( driver );
    }

    protected void after() {
    	driver.quit();
    }

    protected void afterClass() throws Throwable {
    }

}

The caveat with this rule is that is going to be used both as a @Rule and as a @ClassRule, as it has to do different things at different stages of the test execution: configure WebDriverManager before actual tests execution and setup and terminate the associated WebDriver used by Selenide, at the beginning and end of each test. Last but not least, in order to annotate a rule with both @Rule and @ClassRule at the same time, the JUnit version must be at least 4.12.

All in all, the unit test now looks like this:

package com.sweftt.swdm;

import static com.codeborne.selenide.CollectionCondition.size;
import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Selenide.open;

import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;

import com.sweftt.swdm.pages.GooglePage;
import com.sweftt.swdm.pages.SearchResultsPage;
import com.sweftt.swdm.rules.BrowserManagerEnum;
import com.sweftt.swdm.rules.WebDriverManagerRule;


/**
 * Google search test with Selenide.
 */
public class GoogleSearchWithRuleTest {
    
    @ClassRule @Rule
    public static WebDriverManagerRule testRule = new WebDriverManagerRule( BrowserManagerEnum.FIREFOX );
    
    @Test
    public void userCanSearch() {
        GooglePage page = open( "http://google.com", GooglePage.class );
        SearchResultsPage results = page.searchFor( "selenide" );
        results.getResults().shouldHave( size( 10 ) );
        results.getResult( 0 ).shouldHave( text( "Selenide: concise UI tests in Java" ) );
    }

}

Much better than the previous version of the test.

Leave a Reply

Your email address will not be published. Required fields are marked *