Selenium tests in Java

January 6th, 2013 | 13 min read | Integration testing, JUnit, Maven, Selenium, Spring, Testing, Web

In deze tutorial rond testing ga ik een eerste integratie test schrijven. Tot vandaag heb ik me enkel bezig gehouden met unit tests waarvan, zoals ik eerder al zei, enkel kleine stukken code getest worden.

Als we echter de samenhang tussen verschillende zaken willen gaan testen, dan noemen we dat integratietesten. Enkele voorbeelden hiervan zijn tests op de DAO laag (persistentie naar de database) en het testen van de front-end (de grafische elementen).

In deze eerste tutorial rond integratie testen ga ik de front-end testen en daarvoor ga ik Selenium gebruiken.

Heads up!

In deze tutorial wordt met regelmaat verwezen naar mijn vorige Java tutorials. Het is dus handig dat je deze (zeker de eerste) eens doorneemt.

Project opzetten

In dit project ga ik gewoonweg verder met waar we gebleven waren met de code uit Mocking met Mockito. Download dus de code die je onderaan die tutorial kan vinden en hernoem het project indien je dat wenst.

Maven configuratie

In deze tutorial gaan we enkele zaken moeten configureren. Als we de front-end willen testen hebben we namelijk enkele zaken nodig zoals een Selenium server die op het juiste moment start/stopt, een webcontainer (bijvoorbeeld Jetty) die ook op het juiste moment start/stopt en net zoals in de tutorial over JUnit willen we dat er een aparte parameter (skipSTests) komt waardoor we de Selenium testen optioneel kunnen starten/stoppen. Integratie-testen kunnen namelijk vrij lang duren (langer dan unit tests).

Als eerste stap moeten we uiteraard de Selenium dependencies toevoegen om te kunnen werken met Selenium. Hiervoor voegen we volgende dependencies toe:

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-firefox-driver</artifactId>
    <version>${selenium.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>${selenium.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-server</artifactId>
    <version>${selenium.version}</version>
    <scope>test</scope>
</dependency>

Bij je properties plaats je:

<selenium.version>2.28.0</selenium.version>
<skipSTests>true</skipSTests>

Als volgende stap gaan we de Selenium tests skippen uit de normale unit testing fase door de

maven-surefire-plugin

te bewerken. Vervang daarvoor de code van deze plugin door:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.12</version>
    <configuration>
        <excludes>
            <exclude>**/selenium/*Test.java</exclude>
        </excludes>
        <skipTests>${skipTests}</skipTests>
    </configuration>
</plugin>

Zoals je kan zien gaan we alle tests in de selenium map (of package) excluden van de unit tests.  Natuurlijk moeten we nu ook nog op een manier de Selenium tests ergens anders “includen”. Daarvoor maken we gebruik van de

maven-failsafe-plugin

:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.12.4</version>
    <configuration>
        <skipTests>${skipSTests}</skipTests>
        <includes>
            <include>**/selenium/*Test.java</include>
        </includes>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Dit is vrij gelijkaardig aan de configuratie van de

maven-surefire-plugin

behalve dat we hier ook nog de plugin gaan koppelen aan 2 fases, namelijk de fase

integration-test

en

verify

. Dit wilt zeggen dat deze tests enkel uitgevoerd worden tijdens deze fases.

Naast het uitvoeren van de tests moeten we ook ervoor zorgen dat er een bepaalde omgeving klaarstaat om in te werken. Deze omgeving bestaat uit een webcontainer (Jetty) en een Selenium server die eigenlijk gaat handelen als intermedium tussen de Java test classes en de browser.

De configuratie om Jetty op te kunnen starten geven we in via een plugin:

<plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
    <version>6.1.26</version>
    <configuration>
        <scanIntervalSeconds>10</scanIntervalSeconds>
        <stopPort>8005</stopPort>
        <stopKey>STOP</stopKey>
        <contextPath>/</contextPath>
        <skip>${skipSTests}</skip>
    </configuration>
    <executions>
        <execution>
            <id>start-jetty</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <scanIntervalSeconds>0</scanIntervalSeconds>
                <daemon>true</daemon>
            </configuration>
        </execution>
        <execution>
            <id>stop-jetty</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>stop</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Zoals je kan zien is dat al een redelijk complexe Maven configuratie (hiermee zie je eens wat de kracht van Maven is). Wat we eigenlijk doen is de Jetty-server zo configureren dat deze opstart in de

pre-integration-test

fase en stopt tijdens de

post-integration-test

fase, wat toch zeer logisch klinkt.

Ten slotte moeten we ook nog een Selenium server starten, de plugin-configuratie hiervoor is:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>selenium-maven-plugin</artifactId>
    <version>2.3</version>
    <configuration>
        <skip>${skipSTests}</skip>
    </configuration>
    <executions>
        <execution>
            <id>start</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>start-server</goal>
            </goals>
            <configuration>
                <background>true</background>
                <logOutput>true</logOutput>
                <multiWindow>true</multiWindow>
            </configuration>
        </execution>
        <execution>
            <id>stop</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>stop-server</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Ook deze configuratie ziet er al iets moeilijker uit, maar toch zou je bepaalde gelijkaardige zaken moeten zien tegenover vorige plugin, ook hier gaan we namelijk de server starten in de

pre-integration-test

fase en stoppen in de

post-integration-test

fase.

Firefox configuratie

In deze tutorial ga ik Firefox gebruiken als browser om de front-end te testen, niets houd je tegen om andere browsers uit te proberen maar Firefox is naast Internet Explorer de enige officiële plugin (de rest zijn third-party) en de configuratie voor Internet Explorer ligt iets moeilijker.

Als eerste stap gaan we een extra profiel aanmaken voor Firefox, dit is aan te raden zodat eigenlijk alles wat in de test-omgeving gebeurt apart blijft ten opzichte van je standaard Firefox profiel dat je gebruikt voor huis-tuin-keuken toestanden (denk aan plugins, …).

Dit doe je door uit te zoeken waar je Firefox geïnstalleerd hebt en het volgende uit te voeren (Windows toets + R):

C:\program files (x86)\Mozilla Firefox\firefox.exe -P

Waarbij je uiteraard het pad naar Firefox zal moeten aanpassen door jouw eigen locatie.

firefox-create-profile

Als je dat gedaan hebt zou je normaal gezien een venstertje moeten zien waar je kan kiezen voor Create Profile, klik hierop. Je kan het gewoon aanmaken op de manier die je zelf wenst, ik heb het de naam Selenium gegeven en het pad ook zelf aangepast.

firefox-create-profile-2

Kies nu om Firefox te starten onder het profiel dat je net hebt aangemaakt door op Start Firefox te klikken.

firefox-create-profile-3

Eenmaal gestart surf je naar seleniumhq.org, ga je naar Download en kies je voor de download onder Download IDE. Deze plugin zorgt ervoor dat de Selenium server kan communiceren met jouw browser. Er zal gevraagd worden of je deze plugin wilt toestaan en nadien of je Firefox wilt herstarten, doe dit.

firefox-download-client
firefox-download-client-2

Sluit nu je browser en open het opnieuw op dezelfde manier die we eerder gebruikt hebben (met de parameter -P). Kies nu terug je default profile zodat dit terug het standaardprofiel is dat gebruikt wordt als je zelf browst.

Selenium test helper

Selenium is in mijn ogen redelijk “low level” en ik kies er meestal voor om een soort van helper-class te maken die het testen op Selenium iets eenvoudiger maakt.

Maak hiervoor een package

be.g00glen00b.selenium

aan in

src/test/java

en maak hierin een class

SeleniumTest

aan. De code voor deze class is:

package be.g00glen00b.selenium;

import static org.junit.Assert.assertEquals;

import java.io.File;

import org.openqa.selenium.WebDriverBackedSelenium;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;

import com.thoughtworks.selenium.Selenium;
import com.thoughtworks.selenium.SeleniumException;
import com.thoughtworks.selenium.Wait;

public class SeleniumTest {

    private static Selenium selenium;
    private static final String FF_PROFILE = "C:\\Users\\Dimitri\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Selenium";
    private static final String LOCATION = "http://localhost:8080";

    protected static void startup() {
        selenium = new WebDriverBackedSelenium(new FirefoxDriver(
                new FirefoxProfile(new File(FF_PROFILE))), LOCATION);
    }

    protected static void shutdown() {
        selenium.close();
        selenium.stop();
    }

    protected void open(String path) {
        selenium.open(path);
    }

    protected String getSelector(String selector, SelectorType type) {
        if (isPresent(selector) || type == SelectorType.NORMAL) {
            return selector;
        }
        if (isPresent("name=" + selector) || type == SelectorType.NAME) {
            return "name=" + selector;
        }
        if (isPresent("id=" + selector) || type == SelectorType.ID) {
            return "id=" + selector;
        }
        if (isPresent("link=" + selector) || type == SelectorType.LINK) {
            return "link=" + selector;
        }
        if (isPresent("xpath=" + selector) || type == SelectorType.XPATH) {
            return "xpath=" + selector;
        }
        if (isPresent("xpath=html/body/" + selector)) {
            return "xpath=html/body/" + selector;
        }
        if (isPresent("xpath=.//*[@id='" + selector + "']")) {
            return "xpath=.//*[@id='" + selector + "']";
        }
        if (isPresent("xpath=.//*" + selector)) {
            return "xpath=.//*" + selector;
        }
        if (isPresent("css=" + selector) || type == SelectorType.CSS) {
            return "css=" + selector;
        } else {
            throw new SeleniumException("Could not find element");
        }
    }

    protected boolean isPresent(String selector) {
        try {
            return selenium.isElementPresent(selector);
        } catch (SeleniumException ex) {
            return false;
        }
    }

    protected String getSelector(String selector) {
        return getSelector(selector, SelectorType.AUTO);
    }

    protected void type(String selector, String text) {
        selenium.type(getSelector(selector), text);
    }

    protected void click(String selector) {
        selenium.click(getSelector(selector));
    }

    protected void verifyText(String selector, String output) {
        assertEquals(output, selenium.getText(getSelector(selector)));
    }

    protected void waitForElement(final String selector, final SelectorType type) {
        new Wait("Element not present") {
            public boolean until() {
                return selenium.isElementPresent(getSelector(selector, type));
            }
        };
    }
}

Zoals je kan zien encapsuleert deze class eigenlijk de calls naar de Selenium server en maakt het het ook iets eenvoudiger. Zo kan je bijvoorbeeld in Selenium selectors meegeven op enorm veel manieren door er telkens aan mee te geven welk type het is. Ik heb in de methode getSelector() er voor gekozen om dit te vereenvoudigen door ervoor te zorgen dat mijn class zelf uitmaakt welk type het moet zijn.

Een andere interessante methode is

verifyText()

die eigenlijk een enorm veel gebruikte opdracht (controleren of een tekst klopt) eenvoudiger maakt.

Wat je wel nog moet doen is bovenaan de lijn code voor het Firefox profiel te bepalen veranderen, namelijk:

   private static final String FF_PROFILE = "C:\\Users\\Dimitri\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Selenium";

Verander het pad hiervan naar het pad dat je gebruikt hebt om eerder een Firefox profiel aan te maken. Indien je het niet meer weet browse je gewoon naar

%appdata%\Mozilla\Firefox\Profiles

en kijk je hoe de map noemt. Let er wel op dat een backslash reserved is waardoor je deze dus eigenlijk moet escapen (vandaar de

\\

).

Ik maak in deze helper-class ook gebruik van een enumeratie die bepaalt welk selector-type je wenst te gebruiken (indien je dat vast wenst te leggen). Maak dus een enum aan met de naam

SelectorType

in

be.g00glen00b.selenium

met de volgende code:

package be.g00glen00b.selenium;

public enum SelectorType {
    AUTO, NORMAL, CSS, NAME, ID, LINK, XPATH
}

Hierin staan dus eigenlijk alle mogelijk selector-types, persoonlijk gebruik ik meestal xpath omdat dit vrij eenvoudig te achterhalen is in de meeste browser developer tools/web tools.

Test classes

Wat we nu nog moeten maken is een test class voor form.html en eentje voor form2.html, maak daarvoor 2 test cases aan met de naam

FormTest

en

Form2Test

in de package

be.g00glen00b.selenium

. Deze test cases voldoen aan de regels die we bij de maven plugins hebben meegegeven, ze zitten namelijk in een map met de naam selenium (omdat dat deel is van de package naam).

De code voor

FormTest

is:

package be.g00glen00b.selenium;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class FormTest extends SeleniumTest {

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        startup();
    }

    @Before
    public void setUp() throws Exception {
        open("/form.html");
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        shutdown();
    }

    @Test
    public void test() {
        type("tekst", "test");
        click("[@id=\"command\"]/table/tbody/tr[2]/td/input");
        waitForElement("html/body/h1", SelectorType.XPATH);
        verifyText("h1", "De tekst is: test");
    }

}

Wat je hier zou moeten zien is dat deze test class overerft van

SeleniumTest

, waardoor alle helper-methodes dus eigenlijk rechtstreeks aan te roepen zijn.
In de

setUpBeforeClass()

methode gaan we alles opstarten terwijl we in

setUp()

telkens een nieuw browser venster gaan open doen (zodat elke test begin met een “verse” pagina. In tearDownAfterClass() gaan we alles wat we geopend hebben ook weer afsluiten.

Deze test case bestaat maar uit 1 test en dat is controleren of de tekst die we ingevoerd hebben ook als resultaat getoond wordt. Zoals je kan zien wordt de

type()

methode aangeroepen die iets zal invoeren in het tekstveld, de

click()

methode die op de submit knop zal klikken, dan wordt er gewacht tot de volgende pagina geladen is om uiteindelijk te controleren of de tekst ook klopt.

De code voor

Form2Test

is zeer gelijkaardig en is:

package be.g00glen00b.selenium;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class Form2Test extends SeleniumTest {

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        startup();
    }

    @Before
    public void setUp() throws Exception {
        open("/form2.html");
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        shutdown();
    }

    @Test
    public void test() {
        type("form/table/tbody/tr[1]/td[2]/input", "test");
        click("form/table/tbody/tr[2]/td/input");
        waitForElement("html/body/h1", SelectorType.XPATH);
        verifyText("h1", "De tekst is: test");
    }

}

Alles is parallel aan het verloop van

FormTest

, met als enige verschil in de test-methode. Wie zich nog de applicatie herinnert, weet nog dat we in

form.html

gebruik maken van een Spring form en in

form2.html

een zelf gemaakte form. Spring genereert iets andere codes die gebruik maakt van ID’s die we dus kunnen gebruiken in onze xpath selector en die niet beschikbaar is in

form2.html

.

Hiermee is dit project dan volledig afgerond, de structuur van het project zou er moeten uitzien als in:

final-project-structure

Builden

Als laatste stap gaan we natuurlijk ook nog alles eens uitproberen. Als we de applicatie gewoon builden of de unit tests uitvoeren zullen we merken dat dit niet veel verschil geeft (logisch omdat we met een parameter werken). Wie wel goed oplet zal kunnen zien dat de Jetty- en de Selenium-plugin wel aangesproken worden, maar omdat deze geconfigureerd zijn met

<skip>

zullen deze ook gewoon overgeslagen worden.

Indien we echter het volgende commando gebruiken:

mvn clean install -DskipSTests=false

zal je merken dat er wel wat verschil is. De servers worden namelijk wel opgestart en wanneer de tests van start gaan zal je zien dat Firefox geopend wordt en automatisch alles zal invullen en testen (zoals wij dat willen).

Je kan ook gewoon kiezen om de integratie-test fase uit te voeren zonder te packagen of wat dan ook met:

mvn integration-test -DskipSTests=false

Hiermee is dan ook deze tutorial volledig ten einde. Selenium is vrij leuk “speelgoed” als je eens echt aan de slag wilt gaan met het testen van je applicatie. Er bestaan ook browser-plugins voor Google Chrome en Opera (en ook nog een methodologie voor Internet Explorer), dus je kan wel wat proberen.

Download project

Download – selenium-example.rar (8,5 kB)

Back to tutorialsContact me on TwitterDiscuss on Twitter

Profile picture

Dimitri "g00glen00b" Mestdagh is a consultant at Cronos and tech lead at Aquafin. Usually you can find him trying out new libraries and technologies. Loves both Java and JavaScript.