Hinweis: Das Editor / Code Formatter Plugin wandelt leider einige XML-Tags in eine falsche Schreibweise um. Bei Übernahme der Beispiele sind daher die betroffenen Tags zu korrigieren (u.a. dependencyManagement, groupId, artifactId, activeByDefault, systemPropertyVariables, containerProfile, arquillian.launch).
Content Repositories sind komplexe und zentrale Komponenten einer IT Infrastruktur / Content Management Lösung. Dies zieht entsprechend hohe Ansprüche im Punkt Qualitätsmanagement nach sich. Bei Alfresco Entwicklungsprojekten wird überwiegend JUnit eingesetzt um Entwicklertests zu realisieren. Das Alfresco Repository kann dabei innerhalb eines einfachen JUnit-TestCase eines fachlichen Moduls durch die Alfresco-Klasse ApplicationContextHelper im gleichen Prozess gestartet oder in einem eingebetteten Container (z.B. Jetty im Rahmen eines Maven-Builds) betrieben werden. Dabei werden in der Regel individuelle fachliche Komponenten nur isoliert und in einem von einer realistischen Umgebung stark abweichenden Konfiguration (u.a. mit umfangreichen Mocks) getestet. Dies kann dazu führen, dass eine hohe Codeabdeckung und Erfolgsquote von Tests nur eingeschränkt aussagekräftig für die Qualität eines Projekts sind und ein böses Erwachen in dedizierten Test- / Abnahmeumgebungen droht.
Im Rahmen unserer Tätigkeit als Alfresco Partner beschäftige ich mich in meiner verfügbaren Zeit außerhalb von Projekten und Vertrieb unter anderem damit, für Entwickler- und Integrationstests Alfresco mit dem auf JUnit basierenden Framework Arquillian zusammen zu bringen. Die verschiedenen Aspekte, Probleme und Ansätze werde ich versuchen, in spezfischen Posts zusammen zu fassen.
Probleme mit “Embedded Tomcat” Setup
Für viele Entwicklungsprojekte mag ein “Embedded” Setup für die Durchführung von lokalen Tests die einfachste Konfiguration darstellen – sowohl in Bezug auf Unabhängigkeit der Konfiguration vom jeweiligen Setup der Entwickler als auch auf die Roundtrip-Zeit der Testdurchführung. Zusammen mit meinen Kollegen habe ich versucht, ein “Embedded Tomcat” Setup mit Arquillian zusammen zu stellen. Es ist jedoch nicht gelungen, die diversen Classloading Probleme, welche sich bei einer Ausführung von Tomcat im gleichen Prozess ergaben, aufzulösen. Speziell mit diversen XML-APIs, welche sowohl im Tomcat als auch im JDK enthalten sind und von Alfresco referenziert werden, kam es zu großen Inkompatibilitäten zwischen dem Arquillian / Maven / Boot-Classloader und dem Classloader für die Alfresco Webapplikation.
Vorbereitungen Tomcat-Instanz / globale Alfresco Konfiguration
Für die Durchführung von Test in einem “Managed Tomcat” Instanz muss eine lokale Tomcat-Instanz bereitgestellt werden, welche Arquillian zur Laufzeit der Tests anspricht und darauf das Alfresco Repository WAR deployed. Entsprechend der zu testenden Alfrescoversion kommt hier entweder Tomcat 6 oder 7 zum Einsatz. Es ist empfehlenswert, einen eigene Tomcat-Instanz für Arquillian aufzusetzen und nicht den ggf. mit dem Alfresco Installer installierten Tomcat zu verwenden.
Damit Arquilian Alfresco nach dem Start des Tomcat auch auf diesen deployen kann, ist es notwendig, die “Manager” Webanwendung von Tomcat auf diesem bereit zu stellen und eine entsprechende Konfiguration der tomcat-users.xml vorzunehmen. Alle anderen Webanwendungen, welche von Tomcat ausgeliefert werden (ROOT / host-manager o.ä.) können bedenkenlos entfernt werden. Eine einfache Konfiguration der tomcat-users.xml kannwie folgt aussehen:
< ?xml version='1.0' encoding='utf-8'?> <tomcat -users> <role rolename="manager"/> <user username="arquillian" password="arquillian" roles="manager"/> </tomcat> |
Um die Testfälle möglichst frei von umgebungsspezifischer Konfiguration zu halten sollte innerhalb der Tomcatinstanz eine alfresco-global.properties mit einer funktionsfähigen Grundkonfiguration sowie die notwendigen Datenbanktreiber bereitgestellt werden. Diese sind wie gewohnt unter <tomcat>/shared/classes abzulegen. Die Konfiguration der Datenbankanbindung, Ablage von Dokumentinhalten sowie unterstützender Komponenten ist hier vorrangig, allerdings können auch Subsysteme (vor-)konfiguriert werden. Für wahrscheinlich 80 % der Testfälle treffen folgende Grundeinstellungen zu:
- Subsystem “fileServers”: Deaktivierung von CIFS, NFS und FTP (über JUnit i.d.R. nicht getestet)
- Subsystem “email”: Deaktivierung des IMAP / SMTP Servers (über JUnit i.d.R. nicht getestet)
Wird die Subsystemkonfiguration über die korrekte Vorgehensweise, d.h. unter Verwendung von <tomcat>/shared/classes/alfresco/extension/subsystems/<Subsystem>/… durchgeführt, können die Voreinstellungen bei Bedarf in einzelnen Testfällen unter Ausnutzung des ClassLoader-Verhaltens überschrieben werden. Unter keinen Umständen sollte Subsystemkonfigurationen daher in alfresco-global.properties durchgeführt werden (auch unabhängig von Arquillian eher “Bad Practice”).
Projektsetup
Die Einbindung von Arquillian in ein bestimmtes Entwicklungsprojekt kann sehr individuell erfolgen. Zu Demonstrationszwecken dient ein einfaches Maven Java-Projekt als Grundlage der weiteren Ausführungen. Die Integration mit dem Alfresco Maven SDK ist dem geneigten Leser überlassen.
Mit mvn archetype:generate -DgroupId={project-packaging} -DartifactId={project-name} -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
oder dem entsprechenden Wizard-Äquivalent der IDE lässt sich ein einfaches Projekt erstellen. Um Arquillian für den Anwendungsfall von Alfrescotests im Maven Lifecycle zu integrieren, sollte folgende Dependency-Konfiguration vorgenommen werden:
<repositories> <repository> <id>jboss-public-repository</id> <name>JBoss Public Repository</name> <url>https://repository.jboss.org/nexus/content/groups/public</url> </repository> </repositories> <dependencymanagement> <dependencies> <dependency> <groupid>org.jboss.arquillian</groupid> <artifactid>arquillian-bom</artifactid> <version>1.0.4.Final</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencymanagement> <dependencies> <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> <scope>test</scope> </dependency> <dependency> <groupid>org.jboss.arquillian.extension</groupid> <artifactid>arquillian-service-integration-spring-inject</artifactid> <version>1.0.0.Beta1</version> <scope>test</scope> </dependency> <dependency> <groupid>org.jboss.arquillian.extension</groupid> <artifactid>arquillian-service-deployer-spring-3</artifactid> <version>1.0.0.Beta1</version> <scope>test</scope> </dependency> <dependency> <groupid>org.jboss.shrinkwrap.resolver</groupid> <artifactid>shrinkwrap-resolver-impl-maven</artifactid> <scope>test</scope> </dependency> </dependencies> |
Diese Abhängigkeiten bilden die Grundlage für das im Arquillian Test-Lifecycle notwendige Zusammenbauen des Alfresco Deployments über die ShrinkWrap API sowie den Zugriff auf Beans des Alfresco Repository für eingebettete JUnit-Tests. Die Spring-bezogenen Abhängigkeiten können weggelassen werden, falls Arquillian ausschließlich für Remote-Tests verwendet werden soll.
Die Verwendung eines von Arquillian gesteuerten Tomcats bedarf der Erstellung des folgenden Profils:
<profiles> <profile> <id>tomcat-managed</id> <activation> <activebydefault>true</activebydefault> </activation> <dependencymanagement> <dependencies> <!-- Lock the version, since additional dependencies (i.e. from Alfresco) often clash --> <dependency> <groupid>commons-codec</groupid> <artifactid>commons-codec</artifactid> <version>1.5</version> </dependency> </dependencies> </dependencymanagement> <dependencies> <dependency> <groupid>org.jboss.arquillian.container</groupid> <artifactid>arquillian-tomcat-managed-6</artifactid> <version>1.0.0.CR4</version> <scope>test</scope> </dependency> </dependencies> </profile> </profiles> |
Für Tests von Alfresco 4.2 mit Tomcat 7 ist die entsprechend alternative Abhängigkeit arquillian-tomcat-managed-7 zu verwenden.
Die zu verwendende Tomcat-Instanz ist über eine arquillian.xml innerhalb des Classpaths des Projekts zu konfigurieren. Diese Datei kann z.B. unter src/test/resources abgelegt werden, um eine globale Konfiguration für das gesamte Projekt zu liefern, oder ein einem Profil-spezifischen Pfad, um individuelle Konfigurationen je nach aktuellem Entwickler zu realisieren. Eine Profil-spezifische Ablage der Datei ist an sich grundsätzlich nicht notwendig, da sich die verschiedenen Konfigurationen innerhalb dieser Datei durch eine Konfiguration des Surefire Plugins individuell ansprechen lassen. Eine Grundkonfiguration er arquillian.xml kann wie folgt aussehen:
< ?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" xmlns:spring="urn:arq:org.jboss.arquillian.container.spring.embedded_3"> <container qualifier="tomcat-managed-6" default="true"> <configuration> <!-- Must match HTTP port from Tomcat server configuration file --> <property name="bindHttpPort">8680</property> <property name="bindAddress">localhost</property> <!-- The prepared Tomcat instance --> <property name="catalinaHome">D:/Applications/Arquillian/tomcat-repo</property> <property name="javaHome">C:/Program Files/Java/jdk1.6.0_30</property> <!-- Allow generous Heap and PermGen since we may deploy + start Alfresco multiple times --> <property name="javaVmArguments">-Xmx2G -Xms2G -XX:MaxPermSize=1G -Dnet.sf.ehcache.skipUpdateCheck=true -Dorg.terracotta.quartz.skipUpdateCheck=true</property> <!-- Must match configured manager from tomcat-users.xml --> <property name="user">arquillian</property> <property name="pass">arquillian</property> <property name="urlCharset">UTF-8</property> <property name="startupTimeoutInSeconds">120</property> <!-- Local copy of Tomcat server configuration file --> <property name="serverConfig">server.xml</property> </configuration> </container> <extension qualifier="spring"> <!-- Deactive automatic inclusion of Spring artifacts in deployments as Alfresco already contains them --> <property name="auto-package">false</property> </extension> </arquillian> |
Die notwendige server.xml kann aus der konfigurierten Tomcatinstanz in den Classpath des Projekts (src/test/resources) kopiert werden. Aus noch nicht nachvollzogenen Gründen kann hier keine Pfadangabe auf die Datei innerhalb der Tomcatinstanz verwendet werden.
Der Wert des qualifier-Attributs der container-Konfiguration kann wie folgt zum Selektieren eines speifischen Profils aus der POM heraus verwendet werden:
<build> <plugins> <plugin> <groupid>org.apache.maven.plugins</groupid> <artifactid>maven-surefire-plugin</artifactid> <configuration> <systempropertyvariables> <arquillian .launch>${containerProfile}</arquillian> </systempropertyvariables> </configuration> </plugin> </plugins> </build> <profiles> <profile> <id>Dev XY</id> <properties> <containerprofile>xy-tomcat-managed-6</containerprofile> </properties> </profile> </profiles> |
Bei Verwendung der JUnit-Integration in Eclipse ist entsprechend der -Darquillian.launch Parameter in der Startkonfiguration zu setzen.
Ein einfacher REST API Test
Die einfachste Verwendung, welche sich mit Arquillian ohne größere Anpassungen realisieren lässt, sind Tests der REST API von Alfresco. Folgender TestCase kann mit der bisherigen Konfiguration (zzgl. Abhängigkeiten auf org.jboss.resteasy:resteasy-jaxrs und org.json:json) direkt ausgeführt werden:
@RunWith(Arquillian.class) public class SimpleLoginRemoteTest { @ArquillianResource // HTTP base-URl specific for our test deployment private URL baseURL; @Deployment // build the Repository WAR we want to test public static WebArchive createDeployment() throws Exception { // initialize Maven resolver from our project POM (specifically: repository-configuration to retrieve artifacts) final MavenDependencyResolver resolver = DependencyResolvers.use(MavenDependencyResolver.class).loadMetadataFromPom("pom.xml"); // we want a standard Alfresco WAR for our tests - no modifications final File[] files = resolver.artifact("org.alfresco.enterprise:alfresco:war:4.1.4").exclusion("*:*").resolveAsFiles(); // there is a simpler "createFromZipFile" method, but we want to provide a custom webapp-name to avoid deployment conflicts // files[0] is the resolved alfresco.war final WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "SimpleLoginRemoteTest.war").as(ZipImporter.class).importFrom(files[0]).as(WebArchive.class); return webArchive; } @RunAsClient @Test public void testAdminRESTLogin() throws Exception { // use JBoss resteasy-library + org.json (add to POM) to perform a login final ClientRequest loginRequest = new ClientRequest(this.baseURL.toURI() + "s/api/login"); loginRequest.accept("application/json"); final JSONObject loginReqObj = new JSONObject(); loginReqObj.put("username", "admin").put("password", "admin"); loginRequest.body("application/json", loginReqObj.toString()); final ClientResponse< String> loginResponse = loginRequest.post(String.class); Assert.assertEquals("Login failed", 200, loginResponse.getStatus()); } } |
Erläuterungen zum Beispiel:
- Arquillian-spezifische TestCase-Klassen müssen mit einem entsprechenden Arquillian-Runner ausgeführt werden, welcher den Lifecycle des TestCase abweichend vom JUnit-Standard steuert.
- Pro TestCase gibt es eine statische Methode mit @Deployment Annotation, welche das zu testende Artefakt über ShrinkWrap erstellt. Hier können die notwendigen bzw. zu testenden Teilkomponenten individuell zusammengeführt werden.
- Testmethoden mit @RunAsClient Annotation werden von Arquillian innerhalb des JUnit-Prozesses / -Kontext ausgeführt und haben können keinen direkten Zugriff auf Beans des Alfresco Repository nehmen. Fehlt diese Annotation, wird die jeweilige Testmethode über ein automatisch von Arquillian in das WebArchive eingefügtes ArquillianServletRunner-Servlet innerhalb der Alfresco Repository Webanwendung ausgeführt.
- Für Testmethoden, welche via @RunAsClient remote auf das Alfresco Repository zugreifen müssen, wird über ein mit @ArquillianResource annotiertes URL Instanzfeld die HTTP URL für den Kontext der Webanwendung bereitgestellt.
- Bei Verwendung des MavenDependencyResolver auf Basis der Projekt-POM müssen die notwendigen Repositories für Alfresco-Artefakte (z.B. http://artifacts.alfresco.com/…) in der POM eingetragen werden.
Bei der Ausführung über die JUnit-Integration der IDE oder mvn test
startet Arquillian die Tomcatinstanz, baut über die entsprechende Callback-Methode das zu testende Artefakte zusammen und überträgt/startet dieses über die Tomcat Manager Webanwendung. Sofern die Tomcatinstanz konfiguriert wurde, sollte entweder ein grüner Balken im JUnit-Fenster oder ein BUILD SUCCESS auf der Maven Konsole erscheinen.
Ein einfacher (Service-)Bean Test
Sollen mit Arquillian individuelle (Service-)Beans getestet werden muss eine Testklasse direkt auf den Spring-Kontext der Alfresco Repository Webanwendung zugreifen. Entegegen den regulären Alfresco JUnit-Tests (z.B. NodeServiceTest) kann dies nicht über den ApplicationContextHelper erfolgen. Da aufgrund der Mischung von regulären Testmethoden und @RunAsClient-Methoden in einer Klasse dies Klasse in zwei unterschiedlichen Kontexten laufen kann, ist es auch nicht ohne weiteres (sauber) möglich, in einer @Before/@BeforeClass und mithilfe ContextLoader.getCurrentWebApplicationContext() auf den Spring-Kontext zuzugreifen um notwendige Beans zu ermitteln. Mit einer einfachen Anpassung an der Erstellung des Deployments und unter Verwendung der @Autowired sowie @Qualifier Annotationen kann man sich die benötigten Beans durch den ArquillianServletRunner bereitstellen lassen.
@RunWith(Arquillian.class) @SpringWebConfiguration public class SimpleNodeServiceLocalTest { @Deployment public static WebArchive createDeployment() { final MavenDependencyResolver resolver = DependencyResolvers.use(MavenDependencyResolver.class).loadMetadataFromPom("pom.xml"); final File[] files = resolver.artifact("org.alfresco.enterprise:alfresco:war:4.1.4").exclusion("*:*").resolveAsFiles(); final WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "SimpleNodeServiceLocalTest.war").as(ZipImporter.class).importFrom(files[0]).as(WebArchive.class); webArchive.addAsResource("arquillian-alfresco-context.xml", "alfresco/extension/arquillian-alfresco-context.xml"); return webArchive; } @Autowired @Qualifier("NodeService") protected NodeService nodeService; @Autowired @Qualifier("TransactionService") protected TransactionService transactionService; @Test public void testNodeService() throws Exception { Assert.assertNotNull("NodeService not injected", this.nodeService); Assert.assertNotNull("TransactionService not injected", this.transactionService); AuthenticationUtil.setFullyAuthenticatedUser("admin"); try { this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback< Void>() { public Void execute() throws Throwable { final List< StoreRef> stores = SimpleNodeServiceLocalTest.this.nodeService.getStores(); Assert.assertFalse("List of stores is empty", stores.isEmpty()); // check default / standard stores Assert.assertTrue("Store workspace://SpacesStore not contained in list of stores", stores.contains(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE)); Assert.assertTrue("Store archive://SpacesStore not contained in list of stores", stores.contains(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE)); return null; } }, true); } finally { AuthenticationUtil.clearCurrentSecurityContext(); } } } |
Die arquillian-alfresco-context.xml zum Beispiel:
< ?xml version='1.0' encoding='UTF-8'?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:annotation -config /> </beans> |
Erläuterungen zum Beispiel:
- Über die @SpringWebConfiguration Annotation wird deklariert, dass bei Ausführung des Tests im Container Beans aus dem aktuellen Spring-Kontext der Webanwendung benötigt werden. Sofern Servlets individuelle Kontexte hätten, könnten dieser über einen optionalen Parameter gezielt angesprochen werden.
- Mit den @Autowired und @Qualifier werden über die Autowiring-Funktionalität von Spring spezifische Beans in die Instanz der Testklasse injeziert, sofern diese im Container ausgeführt wird. Autowiring ist in Alfresco nicht vorkonfiguriert, weswegen im Deployment eine zusätzliche Spring XML Konfigurationsdatei zum WAR hinzugefügt wird. Diese Datei ändert faktisch nichts an der Spring-Konfiguration von Alfresco, aktiviert aber implizit die Unterstützung von Autowiring.
- Testmethoden laufen grundsätzlich ohne Authentifizierung oder einem Transaktionskontext. Sofern dieser für Tests benötigt wird, muss dieser im Test initiiert werden. Bei Verwendung von öffentlichen Service-Beans (z.B. “NodeService”) kann dieser wegfallen, wenn eine atomare Operation getestet wird, da öffentliche Service-Beans (bei korrekter Konfiguration) automatisch bei einem Aufruf für einen gültigen Transaktionskontext sorgen.
- In der POM des Projekts ist die Alfresco Repository JAR der entsprechenden Alfresco Version als Abhängigkeit einzutragen, damit der Test kompilieren kann.
Offene Punkte / Probleme
Mit den bereit gestellten Beispielen lässt sich Alfresco allumfänglich in einem “Managed Tomcat” Szenario testen. Allerdings gibt es wie bei allen Testansätzen auch hier diverse Punkte / Probleme, welche die Nutzbarkeit / Effizienz einschränken können. Im folgenden seien die aus meiner Sicht kritischsten Punkte genannt:
- Dauer des Deployment – Jedes Deployment einer Testfall-spezifischen Alfresco Repository WAR verbaucht eine enorme Zeit für die Übertragung sowie das Hoch- und Runterfahren Teilkomponenten. Auf meinem Lenovo T520 mit einer aktuellen SSD und 16 GiB RAM beläuft sich ein Durchlauf von 2 Testklassen (Komplexität nicht viel höher als die Beispiele hier) auf ca. 3 Minuten. Für einzelne Komponententests mag dies mit einer kleinen Kaffeepause durchaus noch vertragbar sein – für größere bzw. häufige Integrationstests z.B. in einem Continuous Integration Kontext wäre dies aber ein erhebliches Problem.
- Speicherlast – Das vielfache Durchführen von Deployments in Tomcat erfordert große Mengen an Speicher, speziell für die Permanent Generation. Je nach Ausstattung der Entwicklungsumgebung kann dieser u.U. nicht bereitgestellt werden, sodass nur einzelne / wenige Testfälle in einem Durchgang ausgeführt werden können.
- Deploymentfehler – Bei mehreren Durchläufen sind in meiner Konstellation Fehler beim (Un-)Deployment der Webanwendungen aufgetreten, welche nur durch manuelle Eingriffe bereinigt werden konnten. Für ein Continuous Integration Szenario ist dies nicht akzeptabel.
- Kleinere Inkonsistenzen zwischen Tomcat-Versionen – Bei Test von Alfresco 4.2 auf Tomcat 7 wurde durch Arquillian das benötigte ArquillianServlertRunner Servlet nicht automatisch in die web.xml eingefügt. Dadurch konnten keine Tests gegen (Service-)Beans durchgeführt werden. Die (unschöne) Lösung des Problems bestand darin, eine angepasste web.xml mit expliziter Konfiguration des AruillianServletRunner Servlets in das WebArchive zu integrieren.
- Bereitstellung einer definierten Datenbasis – Für einzelne Tests kann es notwendig sein, dass diese einen definierten Zustand von Daten (Datenbank, Inhalte, Index) vorfinden. Die Datenhaltung erfolgt bei einem “Managed Tomcat” Setup aber außerhalb der Testfalllogik und kann von dieser nicht ohne umgebungspezifischen Code beeinflusst werden.
In weiteren Blog-Posts sollen für einige dieser Punkte mögliche Ansätze / Lösungen beschrieben werden.
0 Kommentare.