07 Feb

Tests con Selenide en Maven

Selenide es un wrapper sobre Selenium que simplifica de sobremanera el uso de éste. Por poner un ejemplo, para soportar un evento AJAX, con Selenium sería algo así:

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"));

Mientras que con Selenide:

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

y básicamente así con todo (en el Github del proyecto se pueden ver muchos más ejemplos).

Selenide logo

Por otra parte, dentro del mundo del testing funcional, existe un patrón muy conocido, Page Objects, que viene a decir que por cada página en la que se quiere llevar a cabo una simulación se crea una clase asociada (un Page Object). Esta clase implementa el acceso a los distintos elementos de la página, lo que ayuda a reducir en los tests el código duplicado, facilitando la reutilización. Con Selenide este patrón tiene esta pinta:

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" ) );
    }

(por defecto, el test se ejecuta usando Firefox, aunque puede cambiarse a través de propiedades de entorno, -Dselenide.browser=ie, o de la clase com.codeborne.selenide.Configuration)

El único problema es que, al ejecutar el test, en un entorno sin configuración adicional, saldrá algo como esto:

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
[omitidas alrededor de 40 líneas de trazas]
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)

El problema es que, para poder ejecutar estos tests (o los de Selenium, que para el caso es lo mismo), es necesario descargar un binario que permite al WebDriver controlar el navegador. Además, es necesario indicar la ruta a estos binarios como propiedades de Java, algo así:

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" );

Desde un punto de vista de ejecución de los tests a través de Maven, con vistas a ejecutarlos en un entorno de integración/despliegue continuo, es problemático dado que los hace dependientes del entorno. Afortunadamente este problema puede evitarse, a través de una librería adicional, WebDriverManager; veamos cómo se integra con Selenide. El código fuente desarrollado está disponible en GitHub.

Lo primero de todo, es incluir la dependencia adecuada, en el caso de Maven:

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

y en el propio test, se configura esta librería:

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 );
    }

    // necesario, al no estar creado por Selenide, es necesario cerrarlo manualmente
    @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" ) );
    }

}

Y con esto se resuelve el problema. Sin embargo, puede hacerse mejor: hay mucha configuración que hay que repetir en cada test. Vamos a externalizar esta configuración a una regla JUnit. En primer lugar se define un enumerado en dónde se van a asociar las distintas clases de configuración de drivers de WebDriverManager con los WebDrivers correspondientes de Selenium:

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;
        }
    }

}

Hecho esto, sólo queda implementar la regla JUnit:

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 {
    }

}

Lo único un poco fuera de lo común es respecto a esta regla es que va a ser usada al mismo tiempo tanto como @Rule como @ClassRule, dado que tiene que hacer cosas distintas en distintos momentos de la ejecución de los tests: configurar WebDriverManager antes de la ejecución en si de los tests y establecer/terminar el WebDriver usado por Selenide, antes y después de la ejecución de cada test. Por último, para poder anotar una regla con @Rule y con @ClassRule al mismo tiempo, la versión a usar de JUnit tiene que ser como mínimo la 4.12.

El test unitario resultante queda del siguiente modo:

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" ) );
    }

}

Mucho mejor que el test anterior.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *