tirsdag 18. desember 2012 Julekalender Java Testing
Kevin Cruickshanks (@kevincruick) er en kompis fra studietiden ved Universitetet i Bergen. Det er ikke så ofte vi ser hverandre lengre, men fra tid til annen stiller han opp på NNUG-møter, og da er det alltid hyggelig.
Arquillian er en spennende plattform for automatiserte integrasjonstester for Java-utviklere. I denne tutorialen git Kevin deg en grundig innføring.
Hvem er du?
Utflytta stording som no er partner og systemutvikler i Machina AS i Bergen
Hva er jobben din?
Systemutvikler på Java og .NET plattform
Hva kan du?
Har mykje erfaring med utvikling av Java server og klienter
Hva liker du best med yrket ditt?
Det er utfordringer kvar einaste dag!
Noe jeg alltid har ønsket meg er et rammeverk for integrasjonstesting av Enterprise Java system (J2EE) i en kontainer. Arquillian er et slikt verktøy som gir deg mulighet for å teste applikasjonene dine rett i en kontainer, som for eksempel Jboss, uten bruk av mockups eller andre omveier for å få koden din testet.
Enhetstester bør du også lage, men det trenger du ikke Arquillian til. Dersom du vil se hvordan applikasjonen din oppfører seg i forskjellige kontainere er Arquillian nettopp noe for deg. Denne artikkelen viser deg hvordan du kan bygge en liten webapplikasjon som kan testes med Arquillian.
Her et utvalg av teknologiene som er i bruk i artikkelen:
For å kunne kjøre Arquillian må vi første importere det. Min webapplikasjon er bygd opp rundt Maven og dermed kan jeg legge til alle eksterne bibliotekreferenser rett i POM-filen min og la Maven laste ned alle nødvendige filer. Arquillian krever en god del tilleggsbiblioteker og kan ta litt tid å laste ned første gang.
Under kan du se et utdrag av POM-filen som omhandler oppsettet for Arquillian. I dependencyManagement angir du hvilken versjon av Arquillian du vil benytte. Alle avhengigheter (dependencies) som ikke har angitt versjon benytter versjonen i dependencyManagement.
JSFUnit biblioteket er et tilleggsbibliotek som kan integreres med Arquillian. JSFUnit er et testrammeverk for JSF webapplikasjoner. Jeg benytter dette rammeverket til testing av websidene i eksempelapplikasjonen. Eksempel på bruk av JSFUnit er omtalt lenger ned i artikkelen.
Profiler
De to profilene som er angitt nederst i utsnittet under, viser hvilke kontainere som kan benyttes til testene. Profilen jbossas-7-remote blir brukt til testing mot eksterne instanser, mens jbossas-7-managed brukes til lokale instanser, dvs, at Arquillian starter og stopper instansen for deg for hver testklasse. Det er desverre ikke mulig å stare en instans av jboss i managed-modus og la den fungere på tvers av flere testklasser. Men dette vil nok bli støttet i senere versjoner av Arquillian.
... <properties> ... <jboss-version>7.1.1.Final</jboss-version> ... </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.jboss.arquillian</groupId> <artifactId>arquillian-bom</artifactId> <version>1.0.1.Final</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.jboss.arquillian.junit</groupId> <artifactId>arquillian-junit-container</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>shrinkwrap-resolver-impl-maven</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.descriptors</groupId> <artifactId>shrinkwrap-descriptors-impl-javaee</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.arquillian.extension</groupId> <artifactId>arquillian-persistence-impl</artifactId> <version>1.0.0.Alpha5</version> </dependency> <dependency> <groupId>org.jboss.jsfunit</groupId> <artifactId>jsfunit-arquillian</artifactId> <version>2.0.0.Beta3-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.spec</groupId> <artifactId>jboss-javaee-6.0</artifactId> <version>3.0.1.Final</version> <scope>provided</scope> <type>pom</type> </dependency> </dependencies> <profiles> <profile> <id>jbossas-7-remote</id> <dependencies> <dependency> <groupId>org.jboss.as</groupId> <artifactId>jboss-as-arquillian-container-remote</artifactId> <version>${jboss-version}</version> <scope>provided</scope> </dependency> </dependencies> </profile> <profile> <id>jbossas-7-managed</id> <dependencies> <dependency> <groupId>org.jboss.as</groupId> <artifactId>jboss-as-arquillian-container-managed</artifactId> <version>${jboss-version}</version> <scope>provided</scope> </dependency> </dependencies> </profile> </profiles> ...
Før vi går i gang med testing må vi få laget til en testbar webapplikasjon. Applikasjonen er bygd på en standard tre-lagsarkitektur med klient-, server- og databaselag. Til database kan du benytte en hvilken som helst SQLkompatibel database. Jeg har til testingen benyttet meg av den innebygde H2-databasen som følger med Jboss7 installasjonen.
JPA er brukt som rammeverk for kommunikasjon med databasen. For å kunne benytte oss av JPA trenger vi en Persistence Unit. Selve filen kan du legge i katalogen src/main/resources/META-INF/. Som tilbyder benytter jeg Hibernate som følger med Jboss. Du trenger ikke inkludere Hibernate i POM-filen din.
Du kan konfigurere og bruke forskjellige i testene dine, men jeg har kun laget til en som er konfigurert til å koble seg opp mot H2-databasen i Jboss.
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="budgettest"> <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>no.ksoft.budget.ds.BudgetPost</class> <class>no.ksoft.budget.ds.Budget</class> <class>no.ksoft.budget.ds.BudgetPostTag</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" /> <property name="hibernate.hbm2ddl.auto" value="create-drop" /> <property name="hibernate.show_sql" value="true" /> </properties> </persistence-unit> </persistence>
Jeg har laget til tre entiter (disse legges til i persistence.xml) til denne applikasjonen som er et forsøk på å lage et lite budsjettprogram:
Under ser du hvordan entiteten Budget ser ut. Alle entitetene arver klassen Any som er en MappedSuperClass. Alle entiteter som arver Any vil få tilordnet alle egenskapene til Any, uten at den er å betrakte som en egen entitet i systemet.
Klassen Any:
@MappedSuperclass @EqualsAndHashCode public class Any { @SuppressWarnings("unused") @Id @Column(name = "id", unique = true, nullable = false) @GeneratedValue @Getter @Setter protected int id; }
Entiteten Budget:
@Entity @Table(name = "budget", schema = "public") @NamedQueries(value = {@NamedQuery(name = "Budget.findAll", query = "SELECT object(b) FROM Budget as b")}) public class Budget extends Any implements java.io.Serializable { @Temporal(TemporalType.TIMESTAMP) @Column(name = "start_date", nullable = false, length = 29) private Date startDate; @Temporal(TemporalType.TIMESTAMP) @Column(name = "end_date", nullable = false, length = 29) private Date endDate; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "budget") private SetbudgetPosts = new HashSet(0); public Budget() { } public Budget(int id, Date startDate, Date endDate) { this.id = id; this.startDate = startDate; this.endDate = endDate; } public Budget(int id, Date startDate, Date endDate, Set budgetPosts) { this.id = id; this.startDate = startDate; this.endDate = endDate; this.budgetPosts = budgetPosts; } ... }
For hver entitet er det opprettet en egen sesjonsbønne (session bean):
Hver bønne implementerer følgende standardmetoder for uthenting og lagring av data:
@Remote public interface Service<T> { T findById(int id); T makePersistent(T entity); List<T> findAll(); }
En ferdigimplementert bønne ser slik ut:
@Stateless public class BudgetServiceImpl implements BudgetService { @PersistenceContext private EntityManager em; @TransactionAttribute(TransactionAttributeType.SUPPORTS) @Override public Budget findById(int id) { return em.find(Budget.class, id); } @Override public ListfindAll() { List l = em.createNamedQuery("Budget.findAll").getResultList(); if (l == null) { return new ArrayList<>(); } return (List ) l; } @Override public Budget makePersistent(Budget entity) { em.persist(entity); return entity; } }
To enkle websider er lagt til. Sidene er kodet i xhtml og begge benytter seg av hver sin Managed bean for uthenting av relevant informasjon fra databasen. Index.xhtml viser et tall for antall budsjetter i databasen og inneholder en knapp som lenker til et vilkårlig budsjett. Den andre websiden, view-budget.xhtml, viser informasjon om et Budsjett som blir hentet ut ved hjelp av en ID paramater.
index.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <head><title>Budgets</title></head> <body> <f:view> <h:form id="viewBudgetForm"> <h1><h:outputText value="Budgets"/></h1> <h:outputText id="totaltAmountOfBudgets" value="#{indexBean.allBudgets.size()}"/> <h:commandLink id="viewAbudget" value="View budget" action="view-budget.xhtml"> <f:param name="budgetId" value="#{indexBean.allBudgets.get(0).id}" /> </h:commandLink> </h:form> </f:view> </body> </html>
view-budget.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <head> <title>Budgets</title> </head> <body> <f:view> <h:form id="viewSingleBudgetForm"> <h:outputText value="Current budget ID: "/> <h:outputText id="currentBudgetId" value="#{viewBudgetBean.currentBudget.id}"/><br /> <h:outputText value="Current budget start date"/> <h:outputText id="currentBudgetStartDate" value="#{viewBudgetBean.currentBudget.startDate}"/><br /> <h:outputText value="Current budget end date"/> <h:outputText id="currentBudgetEndDate" value="#{viewBudgetBean.currentBudget.startDate}"/><br /> </h:form> </f:view> </body> </html>
Det bakomforliggende bønnene (managed beans) benytter CDI for å legge inn referanser til sesjonsbønnene. Bønnen IndexBean benyttes i index.html, mens bønnen ViewBudgetBean benyttes i view-budget.xhtml. Ingen fantasifull navngivning av bønner dette, men navnet mer enn antyder hvor den aktuelle bønnen er i bruk. Du kan selvsagt bruke bønner på tvers av flere sider, men du kan fort ende opp i situasjoner som er vanskelig å håndtere etterhvert som applikasjonen vokser.
Bønnen IndexBean
@Named("indexBean") @RequestScoped public class IndexBean { @Inject BudgetService budgetService; public ListgetAllBudgets() { return budgetService.findAll(); } }
Bønnen ViewBudgetBean
@Named("viewBudgetBean") @RequestScoped public class ViewBudgetBean { @Inject BudgetService budgetService; @Getter @Setter @Inject @RequestParam("budgetId") Instance<Integer> budgetId; public Budget getCurrentBudget() { return budgetService.findById(getBudgetId().get().intValue()); } }
Til testing av serverlaget trenger vi først litt mer konfigurering av Arquillian. Konfigurasjonsfilen Arquillian.xml legges i <sti til prosjektet ditt>src/test/resources. Min fil ser slik ut:
<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"> <engine> <property name="deploymentExportPath">target/</property> </engine> <defaultProtocol type="Servlet 3.0" /> <container qualifier="jbossas-7-managed" default="false"> <configuration> <property name="jbossHome">X:\sti\til\jboss\jboss-as-7.1.1.Final</property> </configuration> <protocol type="jmx-as7"> <property name="javaVmArguments">-Xmx512m -XX:MaxPermSize=128m -Xverify:none -XX:+UseFastAccessorMethods</property> <property name="executionType">REMOTE</property> </protocol> </container> <container qualifier="jbossas-7-remote" default="true"> <configuration> <property name="managementAddress">192.168.1.195</property> <property name="managementPort">8080</property> </configuration> <protocol type="jmx-as7"> </protocol> </container> </arquillian>
Filen inneholder konfigurasjonselementer for begge profilene jeg oppgav i POM-filen (jbossas-7-managed og jbossas-7-remote). For remote-profilen oppgir jeg ip-adressen og portnummeret til den kjørende jbossinstansen, mens for managed, oppgir jeg den fulle stien til jbossinstallasjonen. Feilinformasjonen fra Arquillian er ikke alltid like intuitiv, men den gir nøyakig informasjon dersom deler av konfigurasjonen din er feil.
Du kan lage flere spektakulære tester med Arquillian, men jeg viser kun det grunnleggende her. Testen under viser det som trengs for å kjøre en enkelt testklasse med Arquillian.
Du trenger følgende tre ting for å lage en testklasse:
I testen under har jeg også lagt til klasseannotasjonen @Transactional med TransactionMode.ROLLBACK som parameter. Denne angir at databasen min blir rullet tilbake etter hver testmetode slik at jeg kan være sikker på at databasen er tom for hver test som kjøres. Denne annoteringen kan og ligge på metodenivå. Andre paramtetere som er tilgengelig for denne annotasjonen er COMMIT og DISABLE.
@UsingDataSet("navn på dataset.xml fil") annotasjonen gir deg mulighet for å populere databasen din med verdier før du kjører testene. Bruken av denne annotasjonen er smør på flesk i midt eksempel da den i praksis gir samme resultat som TransactionMode.ROLLBACK siden jeg benytter meg av et tomt dataset før hver test kjøres. Du kan lese mer om bruk av dataset i testene her.
@Deployment-metoden lager et ShrinkWrap arkiv som blir innstallert i kontainerinstansen under kjøring. I eksempelet er det en WAR-fil som blir opprettet. Alle klasser som er nødvendige for å kunne kjøre testen må legges inn i arkivet. Du trenger ikke ta med alle klassene i prosjektet ditt dersom de ikke er knyttet opp mot noen av klassene du trenger for å gjennomføre testene testene.
Det er også mulig å knytte opp hele prosjekt (F.eks.: et Java bibliotekprosjekt) inn i arkivet. Det vil med andre ord kunne være lurt å lage et eget Arquillian testprosjekt som kobler inn det nødvendige web-, ejb- og javabibliotekene du trenger og deretter utfører testene. Eneste forskjellen vil være et mer håndterbart arkiv som du kan benytte i testene dine.
@RunWith(Arquillian.class) @UsingDataSet("empty.xml") @Transactional(TransactionMode.ROLLBACK) public class BudgetServiceTest { private static final Logger LOGGER = Logger.getLogger(BudgetServiceTest.class.getName()); @Inject private BudgetService budgetService; public BudgetServiceTest() { } @Deployment public static Archive<?< createTestableDeployment() { final WebArchive war = ShrinkWrap.create(WebArchive.class, "service.war"); war.addClass(Any.class); war.addClass(Budget.class); war.addClass(BudgetPost.class); war.addClass(BudgetPostTag.class); war.addClass(Service.class); war.addClass(BudgetService.class); war.addClass(BudgetServiceImpl.class); war.addAsResource("META-INF/persistence.xml", "META-INF/persistence.xml"); war.addAsWebInfResource(EmptyAsset.INSTANCE, ArchivePaths.create("beans.xml")); LOGGER.info(war.toString(Formatters.VERBOSE)); return war; } @Test public void testFindAllBudgets() { List<Budget< all = budgetService.findAll(); assertEquals(true, all.isEmpty()); insertABudget(); all = budgetService.findAll(); assertEquals(1, all.size()); } @Test public void testFindBudgetById() { final Budget expected = insertABudget(); assertTrue(expected.getId() < 0); List<Budget< all = budgetService.findAll(); assertEquals(1, all.size()); Budget result = budgetService.findById(expected.getId()); assertEquals(expected, result); } @Test public void testMakePersistent() { Budget expected = insertABudget(); Budget test = budgetService.findById(expected.getId()); assertEquals(expected, test); } private Budget insertABudget() { final Budget b = new Budget(); b.setEndDate(new Date(System.currentTimeMillis())); b.setStartDate(new Date(System.currentTimeMillis())); return budgetService.makePersistent(b); } }
Til slutt vil jeg vise hvordan du kan få testet ut alle dei tre lagene i applikasjonen. Under vises et eksempel på en testklasse for å teste ut manipulering og navigering mellom to websider. Denne testing er mulig ved hjelp av JSFUnit som gir deg tilgang til en del nyttige metoder for å manipulere webapplikasjonen under kjøring i en kontainer.
I metoden testIndexBean(JSFServerSession server, JSFClientSession client) blir server og client injisert inn i metoden. JSFServerSession gir deg tilgang til deler av JSF API`et, mens JSFClientSession er en wrapper-klasse for HtmlUnit som imiterer nettleser interaksjon med en JSF-applikasjon.
@RunWith(Arquillian.class) @InitialPage("/index.xhtml") public class WebTest { private static final Logger LOGGER = Logger.getLogger(WebTest.class.getName()); @Inject private BudgetService budgetService; @Deployment(testable = true) public static Archive>?< createTestableDeployment() { MavenDependencyResolver resolver = DependencyResolvers.use( MavenDependencyResolver.class).loadMetadataFromPom("pom.xml"); final WebArchive war = ShrinkWrap.create(WebArchive.class, "budget.war"); war.addClass(Any.class); war.addClass(Budget.class); war.addClass(BudgetPostTag.class); war.addClass(BudgetPost.class); war.addClass(Service.class); war.addClass(BudgetService.class); war.addClass(BudgetPostService.class); war.addClass(BudgetPostTagService.class); war.addClass(BudgetServiceImpl.class); war.addClass(BudgetPostServiceImpl.class); war.addClass(BudgetPostTagServiceImpl.class); war.addClass(IndexBean.class); war.addClass(ViewBudgetBean.class); war.addClass(FieldParameterNames.class); war.addAsWebResource(new File("src/main/webapp", "index.xhtml")); war.addAsWebResource(new File("src/main/webapp", "view-budget.xhtml")); war.setWebXML(new File("src/main/webapp/WEB-INF/web.xml")); war.addAsWebInfResource(new File("src/main/webapp/WEB-INF/faces-config.xml"), "faces-config.xml"); war.addAsWebInfResource(new File("src/main/webapp/WEB-INF/jboss-web.xml"), "jboss-web.xml"); war.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); war.addAsResource("META-INF/persistence.xml", "META-INF/persistence.xml"); war.addAsLibraries(resolver.artifact("org.jboss.solder:solder-impl").resolveAsFiles()); LOGGER.info(war.toString(Formatters.VERBOSE)); return war; } @Before public void addSomeValues() { Budget b = new Budget(); b.setEndDate(new Date(System.currentTimeMillis())); b.setStartDate(new Date(System.currentTimeMillis())); budgetService.makePersistent(b); } @Test @InitialPage("/index.xhtml") public void testIndexBean(JSFServerSession server, JSFClientSession client) throws IOException { //Sjekk at vi er på indexsiden assertEquals("/index.xhtml", server.getCurrentViewID()); //Sjekk at IndexBean får hentet ut det ene budsjettet vi har opprettet assertEquals(1, server.getManagedBeanValue("#{indexBean.allBudgets.size()}")); UIComponent totalAmountOfBudgetsOutput = server.findComponent("totaltAmountOfBudgets"); //Sjekk at antall budsjett vises korrekt på indexsiden assertEquals("1", totalAmountOfBudgetsOutput.getAttributes().get("value").toString()); //Klikk på vis budsjettknappen client.click("viewBudgetForm:viewAbudget"); //Sjekk at vi har skiftet side til view-budget.xhtml assertTrue(server.getCurrentViewID().contains("view-budget")); } }
Kildekoden til denne artikkelen er tilgjengelig som en github repository.