Integračné testy s Arquillianom

U nás vo 4Q kladieme veľký dôraz na kvalitu výsledného produktu. Písanie automatizovaných testov je jedným z mnohých nástrojov, ktorým sa dá zabezpečiť korektnosť kódu voči špecifikácii.

Jedným z pohľadov, akým sa možno na testovanie aplikácie pozrieť, sú jednotkové (unit) testy. Ďalšou úrovňou testovania sú integračné testy. Zámerne spomínam oba práve preto, aby som ich mohol porovnať. Pri unit testoch skúmame integritu kódu na úrovni jednotlivých metód a ich správneho fungovania. Čo sú to však integračné testy? Pri integračných testoch, na rozdiel od unit testov, skúmame funkčnosť jednotlivých komponentov aplikácie pokiaľ možno v najvernejšom prostredí, ktoré sa blíži k reálnym podmienkam produkčnej prevádzky. Pri webových aplikáciach ide najmä o testovanie jednotlivých URI a odkontrolovanie ich odpovedí.

J2EE a testovanie

Opäť sa vrátim k unit testom. Unit testy sa v prostredí J2EE servera obvykle píšu spôsobom, pri ktorom sú jednotlivé mechanizmy J2EE servera nahradené „dummy“ implementáciami, volania na rôzne ďalšie subsystémy sú nahradené známymi odpoveďami a celkovo je množstvo faktorov schválne vynechávaných len preto, aby sme otestovali funkčnosť jednotlivých tried a ich metód. Pri integračných testoch však narážame na problém, že tieto komponenty by vynechané byť nemali. Akým spôsobom zabezpečiť čo najvernejšiu „simuláciu“ J2EE servera? Riešení je mnoho, my sme zvolili testovací framework Arquillian. Arquillian totiž dokáže zabezpečiť spúšťanie integračných testov priamo v J2EE serveri. Ďalšou výhodou Arquillianu je tiež to, že tieto testy sú súčasťou kódu aplikácie. To znamená, že je možné ich spúšťať rovnakým spôsobom ako unit testy. S využitím vývojového nástroja Maven je teda tiež možné efektívne vytvoriť build server, ktorý bude tieto testy automaticky spúšťať (nezávislé od toho, že si môže každý jeden developer pracujúci na projekte priebežne pomocou týchto testov kontrolovať, či svojimi zmenami v kóde nenaruší už existujúce časti aplikácie). Keďže pridanie podpory Arquillianu do projektu môže byť na prvýkrát mätúce, uvedieme si krok za krokom postup, ako na to v J2EE aplikačnom serveri Wildfly 8.2. Na správu závislostí a zostavenie projektu použijeme Maven.

Prvé kroky implementácie

V prvom kroku je potrebné mať k dispozícii existujúci projekt. Postačí J2EE aplikácia s ľubovolnou architektúrou. Predstavme si teda našu testovaciu aplikáciu – Zápisník. Zápisník bude riešiť CRUD (Create, Read, Update, Delete) pre 1 triedu – poznámku (Note). Na to obsahuje štandardné REST rozhranie.

Pridanie závislostí pre Arquillian

V projekte si nájdeme pom.xml súbor. Na to aby sme zahrnuli do projektu arquillian potrebujeme spraviť niekoľko úprav. Tou prvou je do sekcie <dependencyManagment> pridať závislosť na arquillian-bom. BOM súbor nahrádza manuálne pridávanie závislostí na viacero artefaktov.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jboss.arquillian</groupId>
            <artifactId>arquillian-bom</artifactId>
            <version>1.1.5.Final</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

Ďalej je potrebné pridať knižnicu na integráciu s frameworkom pre testy. My používame JUnit, takže pridáme potrebnú knižnicu.

<dependency>
    <groupId>org.jboss.arquillian.junit</groupId>
    <artifactId>arquillian-junit-container</artifactId>
    <scope>test</scope>
</dependency>
<!-- Tieto zavislosti su nepovinne ale neskôr ich používam -->
<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-api</artifactId>
    <version>2.2.0-beta-2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-api-maven</artifactId>
    <version>2.2.0-beta-2</version>
    <scope>test</scope>
    </dependency>
<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
    <scope>test</scope>
    <version>2.2.0-beta-2</version>
</dependency>
<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-spi</artifactId>
    <scope>test</scope>
    <version>2.2.0-beta-2</version>
</dependency>

A teraz prichádza to podstatné.. Je potrebné pridať závislosti na adaptér na server, v ktorom chceme testy spúšťať. Avšak je zrejmé že spúšťanie integračných testov môže trvať dosť dlho v závislostí od aplikácie. Nebolo by teda múdre spúšťať testy pri každom zostavení. Tento problém sa dá vyriešiť pomocou profilov. V profile no-integration-test nie je potrebné nič špeciálne nastavovať (aj keď by sa dal použiť default-configuration profil, osobne som radšej keď sa vytvorí profil navyše, s jasným názvom a s plnou kontrolou čo je v tom profile nastavené.)

<profile>
    <id>no-integration-tests</id>
</profile>
<profile>
    <id>integration-tests-arquillian-wildfly-remote</id>
    <dependencies>
        <dependency>
            <groupId>org.jboss.spec</groupId>
            <artifactId>jboss-javaee-7.0</artifactId>
            <version>1.0.3.Final</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.wildfly</groupId>
            <artifactId>wildfly-arquillian-container-remote</artifactId>
            <version>8.2.0.Final</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.18.1</version>
                <configuration>
                    <forkCount>1</forkCount>
                    <systemPropertyVariables>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                        <jboss.home>${wildfly-home}</jboss.home>
                        <module.path>${wildfly-home}/modules</module.path>
                    </systemPropertyVariables>
                    <redirectTestOutputToFile>false</redirectTestOutputToFile>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

Ako vidíte, v kóde som pridal závislosť na arqullian-wildfly-remote. To preto, že chcem využiť server na ktorom reálne spúšťam aplikáciu, nie jeho vstavanú (embedded) verziu – sú zdokumentované prípady keď boli embedded verzie nestabilné a neposkytovali verné testy – záleží to však na situácii. Ďalej konfigurujem plugin s názvom failsafe. Mnoho ľudí pozná plugin surefire no nepoznajú jeho „brata“ failsafe. Rozdiel medzi nimi je ten, že failsafe je určený nie na unit testy ale na integračné testy. Zaujímavé je okrem sémantického hľadiska aj to, že failsafe aj pri zlyhanom teste spustí jednu dodatočnú fázu buildu, a až potom vo fáze verify ukončí build ako FAILED. Na túto dodatočnú fázu (post-integration-test) je možné napríklad zavesiť ďalší plugin, ktorý bude „upratovať.“ Navyše nám to dovoľuje mať od seba oddelené unit a integračné testy aj na úrovni spúšťania.

arquillian.xml

Tento súbor je nepovinný no aj tak ho okrajovo spomeniem, keďže v mnohých prípadoch sa môže veľmi hodiť. S defaultnými nastaveniami sa v arquilliane dostanete veľmi ďaleko. Dvojnásobne to platí o remote serveroch, ktoré máte (alebo by ste mali mať) nastavené pomocou konfiguračného xml. V týchto prípadoch je používanie tohto súboru skoro zbytočné, no môže sa hodiť pre špecifické nastavenia správania sa arquillianu. Pre embedded servre sa často používa na špecifikovanie cesty ku konfiguračnému súboru, dynamickému vkladaniu JDBC nastavení pre testovacie účely a pod. Možností použitia je mnoho a nastavenia závisia aj od použitého adaptéru na daný kontajner.

Testovacie triedy

Toto je miesto kde sa deje mágia. Vytvorenie testovacej triedy je skutočne jednoduché. Arquillian si kladie 3 podmienky.

  • Trieda musí byť anotovaná @RunWith(Arquillian.class)
  • Trieda musí obsahovať statickú metódu anotovanú @Deployment ktorá vracia objekt triedy JavaArchive (prípadne potomkov tejto triedy) – ide o tzv. micro-deployment. Tento bude nasadený na server. V prípade veľkých a rozsiahlych aplikácii s množstvom tried, servletov a podobne, máme vďaka tomu možnosť nasadzovať len funkcionalitu, ktorá bude testovaná
  • Trieda musí obsahovať aspoň jednu metódu anotovanú ako @Test

Samotný Arquillian umožňuje vytvoriť dva typy testov.

  • Server-side testy

Pri týchto testoch väčšinou sledujeme otestovanie funkčnosti rôznych vrstiev aplikácie – DAO, service a pod. Je v nich tiež možné priamo v testovacej triede využívať všetky funkcionality, ktoré nám poskytuje server (EJB, CDI, …)

  • Client-side testy

Tieto testy sú z pohľadu integrity aplikácie asi o niečo zaujímavejšie. Z pohľadu sémantiky Arquillianu je potrebné mať testovaciu triedu alebo vybrané metódy anotované ako @RunAsClient. Následne strácame možnosť používať funkcionalitu kontajneru priamo v testovacej triede (metóde). Získame však možnosť použiť zaujímavú anotáciu @ArquillianResource, ktorou vieme získať URL na ktorej je nasadený daný micro-deployment. Takže vieme jednoducho a pohodlne otestovať vonkajšie API našej aplikácie. Na záver už len dodám príklad najjednoduchšej testovacej triedy pre Zápisník s integračným testom pre overenie funkčnosti REST volania na GET /notes.

@RunWith(Arquillian.class)
@RunAsClient
public class ZaspisnikCrudIT {
    @ArquillianResource
    private URL APPLICATION_URL;
    @Deployment
    public static WebArchive createDeployment() {
        WebArchive archive;
        archive = ShrinkWrap.create(WebArchive.class, "test.war");
        archive.addPackages(true, "sk.fourq.zapisnik") 
                .addAsWebInfResource(new FileAsset(new File("src/main/webapp/WEB-INF/beans.xml")), "beans.xml")
                .addAsLibraries(Maven.resolver().loadPomFromFile("pom.xml").importRuntimeDependencies().resolve().withTransitivity().asFile());
        LOG.info("WAR BUILDING FINISHED");
        return archive;
    }
    @Test
    public void getNotes() {
        try {
            URL restUrl = new URL(APPLICATION_URL + "notes");
            Response response = getHttpClient().target(restUrl.toURI()).request().get();
            Assert.assertEquals(HttpCode.OK, response.getStatus());
        } catch (MalformedURLException | URISyntaxException e) {
            Assert.fail(e.getMessage());
        }
    }

    /**
     * Poskytnutie HTTP Klienta pre testovacie metody
     *
     * @return HTTP client (Podla Jax-RS Client špecifiácie)
     */
    private Client getHttpClient() {
        return ClientBuilder.newClient();
    }
}