Month: March 2013

Arquillian in testiranje storitev REST

Posted on

Arquillian je orodje za integracijsko testiranje komponent. V najbolj osnovni obliki je odgovor na vprašanje: kako naj stestiram nek EJB v njegovem okolju ? Članek ni namenjen osnovam Arquilliana, te lahko spoznate na:  http://jaxenter.com/arquillian-a-component-model-for-integration-testing.1-35304.html, ampak je namenjen prikazu testiranja:

storitve REST, ki je implementirana nad Stateless EJBjem, in glede na vhodni parameter poišče (preko JPA) določeno entiteto in jo vrne.

Za testiranje (za odjemalca) bomo uporabili knjižnico RESTEasy (http://www.jboss.org/resteasy/), za pretvorbo JSON v Java objekte in obratno, pa bomo uporabili knjižnico Jackson (http://jackson.codehaus.org).

Sam Arquillian pa bo konfiguriran v načinu Glassfish 3.1 Managed, več o tem na https://docs.jboss.org/author/display/ARQ/GlassFish+3.1+-+Managed.

Implementacija storitve REST

Da je REST sploh omogočen, moramo definirati t.i. Application Path:

@ApplicationPath("/resources")
public class RestApplication extends Application {}

nato lahko implementiramo storitev, sicer s pomočjo Stateless EJBja:

@Stateless
@Path("/alert")
public class TestResource {

    @Inject
    @PuAdminWH
    EntityManager entityManager;

    @GET
    @Path("/{id}")
    @Consumes("text/plain")
    @Produces("application/json")
    public AppJmsAlert getAlertById(@PathParam("id") int id) {

        //Tole lahko vrne NoResultException, glej Exception mapper        AppJmsAlert alert = ...koda za poizvedbo...

        return alert;
    }
}

Vidimo, da smo implementirali klic tipa GET, ki sprejme en parameter id tipa text in vrne JSON prezentacijo entitete JPA tipa AppJmsAlert, torej naš klic bo izgledal takole:

http://localhost:8080/.../resources/alert/id/1453

V primeru, da vhodni parameter ne vrne rezultata (ne vrne entitete JPA), se bo avtomatsko na strežniku sprožil NoResultException, odjemalcu pa v takem primeru želimo vrniti odgovor s statusom HTTP tipa BAD_REQUEST. Zato moramo implementrati t.i. Exception Mapper:

@Provider
public class NoResultExceptionMapper implements ExceptionMapper<NoResultException> {

    @Override
    public Response toResponse(NoResultException e) {

        return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
    }
}

Konfiguracija orodja Arquillian

Na lokacijo src/test/resources ustvarimo dve datoteki:

 arquillian.launch in arquillian.xml

ki nam predstavljata konfiguracijo. V launch datoteko bomo vpisali t.i. Container Qualifier, ki določa, katera konfiguracija, določena v arquillian.xml, je aktualna ob zagonu testov. Zato bo njena vsebina naslednja:

remote-gf

Arquillian.xml pa ima naslednjo vsebino:

<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns="http://jboss.org/schema/arquillian"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

    <container qualifier="remote-gf" default="true">
       <configuration>
          <property name="glassFishHome">
               /Applications/NetBeans/glassfish-3.1.2.2
          </property>
       </configuration>
    </container>
</arquillian>

vidimo, da je potrebno definirati pot do kontejnerja EE, v tem primeru Glassfish, katerega bo Arquillian zagnal, v njega namestil testni war in zagnal teste.

To pa ni vse, sledi najtežji del, sicer pravilna nastavitev pom.xml-ja:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.8.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.jboss.arquillian.junit</groupId>
    <artifactId>arquillian-junit-container</artifactId>
    <version>1.0.3.Final</version>
    <scope>test</scope>
</dependency>    

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxrs</artifactId>
    <version>2.3.4.Final</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jackson-provider</artifactId>
    <version>2.3.4.Final</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-api-maven</artifactId>
    <version>2.0.0-alpha-1</version>
    <scope>test</scope>
    <type>jar</type>
</dependency>    

<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
    <version>2.0.0-alpha-1</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>com.google.collections</groupId>
            <artifactId>google-collections</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Vidimo, da smo na classpath dali naslednje stvari:

  • junit in arquillian container;
  • implementacijo resteasy in jackson;
  • shrinkwrap resolver, katerega si bomo pogledali kasneje.

Dodatno bomo definirali maven profile, v katerem pa imamo naslednjo definicijo:

<profiles>  
    <profile>
        <id>glassfish-managed-3.1</id>
        <dependencies>
            <dependency>
                <groupId>com.inteligoo</groupId>
                <artifactId>intelicommon</artifactId>
                <version>1.0</version>

                <!--
                  Ker spodaj include.dam
                -->
                <exclusions>
                    <exclusion>
                        <groupId>javax</groupId>
                        <artifactId>javaee-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-log4j12</artifactId>
                    </exclusion>
                </exclusions> 
           </dependency>    
           <dependency>
               <groupId>org.jboss.arquillian.container</groupId>
               <artifactId>arquillian-glassfish-managed-3.1</artifactId>
               <version>1.0.0.CR3</version>
               <scope>test</scope>
               <exclusions>
                   <exclusion>
                       <groupId>org.jboss.arquillian.container</groupId>
                       <artifactId>arquillian-container-spi</artifactId>
                   </exclusion>  
                   <exclusion>
                       <groupId>com.sun.jersey</groupId>
                       <artifactId>jersey-bundle</artifactId>
                   </exclusion>
                   <exclusion>
                       <groupId>com.sun.jersey.contribs</groupId>
                       <artifactId>jersey-multipart</artifactId>
                   </exclusion>
             </exclusions>
         </dependency>

         <!--
           Nadomestim z novejsimi, sicer imam error:
org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData.getContexts(Ljava/lang/Class;)Ljava/util/Collection;
         -->
         <dependency>
             <groupId>org.jboss.arquillian.container</groupId>
             <artifactId>arquillian-container-spi</artifactId>
             <version>1.0.3.Final</version>
             <scope>test</scope>
         </dependency>

         <!--
           Nadomestim z novejsimi, sicer imam error:
             com.sun.jersey.api.client.ClientHandlerException:

A message body writer for Java type, class com.sun.jersey.multipart.FormDataMultiPart, and MIME media type, multipart/form-data, wasnot found
          -->       
         <dependency>
             <groupId>com.sun.jersey</groupId>
             <artifactId>jersey-bundle</artifactId>
             <version>1.12</version>
             <scope>test</scope>
         </dependency>

         <dependency>
             <groupId>com.sun.jersey.contribs</groupId>
             <artifactId>jersey-multipart</artifactId>
             <version>1.12</version>
             <scope>test</scope>
         </dependency>      

         <!--
           Sicer mam error:
             Absent Code attribute in method...
         -->
         <dependency>
             <groupId>org.jboss.spec</groupId>
             <artifactId>jboss-javaee-6.0</artifactId>
             <version>1.0.0.Final</version>
             <type>pom</type>
             <scope>provided</scope>
         </dependency>      
     </dependencies>

     <build>
         <testResources>
             <testResource>
                 <directory>src/test/resources</directory>
             </testResource>
             <testResource>
                 <directory>src/test/resources-glassfish-managed</directory>
             </testResource>
         </testResources>
      </build>
   </profile>
</profiles>

POZOR! Obvezno je potrebno podrobno pregledati zgornji odsek kode, namreč v komentarjih so opisani vsi problemi, na katere sem naletel. Zato je potrebno uporabiti tudi nekaj exclusion-ov, da se stvar sploh hoče zagnati.

Priprava testov

Da lahko testiramo našo storitev s RESTEasy, moramo implementirati t.i. razred stub, ki bo samo ogrodje za našo storitev:

@Path("/alert")
public interface TestResourcesClientStub {

    @GET
    @Path("/{id}")
    @Consumes("text/plain")
    @Produces("application/json")
    public AppJmsAlert getAlertById(@PathParam("id") int id);  

}

Ogrodje bomo uporabili v naših testih Arquillian:

@RunWith(Arquillian.class)
@RunAsClient

public class TestResourceTest {   

    private static final String RESOURCE_PREFIX = RestApplication.class.getAnnotation(ApplicationPath.class).value().substring(1);

    /**
    * Resource, ki vrne npr.: http://localhost:8080/test/ v primeru, da deployamo test.war
    * V primeru Glassfish Embeddable, nam ta resource ne vrne prave vrednosti
    */
    @ArquillianResource
    private URL url;

    @Deployment(testable=false)
    public static WebArchive deploy(){   

        //0.) Za dodajanje poljubnih artifaktov iz POMa v obliki knjiznic
        final MavenDependencyResolver resolver = DependencyResolvers.use(MavenDependencyResolver.class);

        //1.) Izdelam arhiv
        WebArchive war = ShrinkWrap.create(WebArchive.class, "test.war");

        //2.) NASTAVITVE
        war.addClasses(SystemConfiguration.class, PuAdminWH.class, RestApplication.class)

        //ENTITETE
           .addPackage(AppJmsAlert.class.getPackage())   
           .addClasses(KpiReportHelper.class, KpiSettings.class)

        //REST CLASSI
           .addClasses(NoResultExceptionMapper.class, TestResource.class)

        //VSE POTREBNE KNJIZNICE: HIBERNATE + JACKSON (pretvorba objektov v JSON)
           .addAsLibraries(resolver.artifacts("org.hibernate:hibernate-core:4.1.0.Final", "org.hibernate:hibernate-entitymanager:4.1.0.Final", "org.jboss.resteasy:resteasy-jackson-provider:2.3.4.Final").resolveAsFiles())

       //BAZA
           .addAsResource("test-persistence.xml", "META-INF/persistence.xml")

       //CDI   
           .addAsWebInfResource(EmptyAsset.INSTANCE, ArchivePaths.create("beans.xml")); 

         return war;  
    }

    /**
    * Inicializacija REST EASY + JACKSON (pretvorba iz JSONa v objekte)
    */
    @BeforeClass
    public static void initResteasyClient() {

        ResteasyProviderFactory instance = ResteasyProviderFactory.getInstance();
        instance.registerProvider(ResteasyJacksonProvider.class);
        RegisterBuiltin.register(instance);
    }

    /**
    * Testiram v primeru, ko mi storitev vrne alert s pričakovanim IDjem
    *
    */
    @Test
    public void testIdExists() {
        int idExists = 490;
        
        //resteasy client
        TestResourcesClientStub client = ProxyFactory.create(TestResourcesClientStub.class,  url.toString() + RESOURCE_PREFIX);

        AppJmsAlert alert = client.getAlertById(idExists);
        Assert.assertEquals(idExists, alert.getIdAppJmsAlert().intValue());

    }

    /**
    * Testiram response storitve, ko alert z določenim idjem ne obstaja
 
    *
    */
    @Test
    public void testBadResponse() {

        int idDoesntExists = 41290;

        //resteasy client
        TestResourcesClientStub client = ProxyFactory.create(TestResourcesClientStub.class,  url.toString() + RESOURCE_PREFIX);

        try {
            AppJmsAlert alert = client.getAlertById(idDoesntExists);
        }
        catch(ClientResponseFailure e) {

            // to je ok
            if(e.getResponse().getResponseStatus().equals(Response.Status.BAD_REQUEST)){
                return;
       }
    }
    Assert.fail("No bad response for id=" + idDoesntExists);
  }
}

Pomebnejše stvari so naslednje:

  • Anotacija @ArquillainResource nam vrne del URLja, na katerem zaganjamo našo testno aplikacijo;
  • RESOURCE_PREFIX nam vrne osnovni del URLja za REST, v našem primeru je to niz po vrednosti “resources”;
  • test bomo izvajali kot odjemalec (@RunAsClient);
  • paket war po imenu “test.war”, v katerem bo naš resurs REST, ki ga testiramo, se izdela s pomočjo Maven Resolverja, ki nam omogoča, da dodamo v naš war vse pakete, neposredno iz maven repozitorija;
  • test-persistance.xml je popolnoma enak persistance.xml, saj namen tega testa ni priprava ustrezne testne baze, ampak bomo uporabili kar obstoječo bazo. Splošno to ne velja in je potrebno pripraviti tudi ustrezno testno bazo;
  • v test.war je potrebno dodati vse razrede, ki jih bomo uporabljali v testu;
  • @BeforeClass anotacija nam označuje metodo, v kateri definiramo odjemalca RESTEasy, s podporo za knjižnico JACKSON;
  • implementirali smo dva testa, sicer v prvem testu pričakujemo, da nam storitev vrne točno določeno entiteto s točno določenim IDjem, v drugem testu pa pričakujemo, da nam storitev vrne BAD_REQUEST, saj poizvedbo pripravimo tako, da namenoma damo napačen id kot parameter.

Pri zagonu našega testa, se tako zgradi paket test.war, se zažene Glassfish 3.1, v katerega se omenjeni paket namesti, in kot odjemalec se zaženeta oba testa. Sami testi niso nič posebnega, najtežji del je žal konfiguracija vseh potrebnih knjižnic (pom.xml), saj privzete knjižnice, kot so navedene na domači strani Arquilliana žal ne delujejo out-of-the-box.

Vidimo tudi, da smo definirali pot do resursov za naše teste (src/test/resources-glassfish-managed) v katerem imamo test-persistance.xml, za našo testno okolje.