Gradle im Praxistest – Teil 2

Moritz Zimmer, Marek Iwaszkiewicz

Im Rahmen unseres Praxistests sind wir im ersten Artikel auf das Aufsetzen einer Java-EE-Anwendung und deren Entwicklung mit Gradle eingegangen. Im zweiten Teil möchten wir tiefer einsteigen und Aspekte rund um Qualitätssicherung, Continuous Integration sowie der Konfiguration unterschiedlicher Zielsysteme beleuchten. Bietet Gradle auch hier die notwendigen Mittel zur professionellen Anwendungsentwicklung?

Im zweiten Artikel demonstrieren wir weiterhin praxisnah anhand unserer Beispielanwendung [1] zuerst die Realisierung der Testautomatisierung. Neben der Umsetzung der wichtigen Integrationstests beschreiben wir auch, wie mit Gradle [2] im Rahmen der Testausführung Aspekte wie Testabdeckung sowie die Ermittlung der statischen Codequalität umgesetzt werden können und hierbei von Mechanismen wie der Testparallelisierung Gebrauch gemacht werden kann. Anschließend werfen wir einen Blick auf die Anbindung von Continuous-Integration-Systemen und demonstrieren abschließend die Erstellung umgebungsspezifischer Konfigurationen.

Artikelserie
Teil 1: Projektbeschreibung, Entwicklungsumgebung, Anwendungsentwicklung
Teil 2: Qualitätssicherung, CI, umgebungsspezifische Konfiguration

Testautomatisierung

Testautomatisierung ist von zentraler Bedeutung für die Sicherstellung der Qualität der entwickelten Software und muss daher auch von jedem Build-System entsprechend unterstützt werden. Was zunächst nach einer einfachen Anforderung klingt, stellt sich bei genauerer Betrachtung als anspruchsvoll heraus. So geht es bei der Testautomatisierung um mehr als nur die einfache Ausführung von Unit Tests. Um eine möglichst optimale Testumgebung zu erhalten, sollte das eingesetzte Build-System zum Beispiel auch die Möglichkeit zur Ausführung von Integrationstests bieten. Idealerweise sollte die Möglichkeit bestehen, einfache Unit Tests und Integrationstests voneinander zu trennen – sowohl in der Ausführung als auch im Bereich des Sourcecodes. Gradle bietet hier für beide Punkte eine Lösung. Zum einen können auf einfache Art und Weise über Source-Sets für alle Projekte Ordner für den Sourcecode und Ressourcen von Integrationstests erstellt werden. In Listing 1 definieren wir für alle Projekte das Source-Set integrationTest. Um eine von Unit Tests losgelöste Ausführung zu erlangen, implementieren wir die gleichnamige Gradle-Task integrationTest vom Typ Test, in der wir angeben, dass nur die Testklassen aus dem eben definierten Source-Set berücksichtigt werden. Die Ausführung der Integrationstests erfolgt mit folgendem Befehl:

 $ gradlew integrationTest

 Mit dem Ausdruck check.dependsOn integrationTest in Listing 1 wird dafür gesorgt, dass die Integrationstests auch bei Ausführung der Check-Tasks angestoßen werden.

    sourceSets { integrationTest {} }

    task integrationTest(type: Test, dependsOn: jar) {
    testClassesDir = sourceSets.integrationTest.output.classesDir

classpath = sourceSets.main.output 
                 + sourceSets.integrationTest.output 
                 + configurations.integrationTestRuntime
  }
  check.dependsOn integrationTest

  def forks = Math.max(2, (int) (Runtime.runtime.availableProcessors() / 2))
  test {
    // JaCoCo Agent zur Bestimmung der Testabdeckung
    jvmArgs "-javaagent:$configurations.jacoco.asPath=destfile=$buildDir/jacoco.exec"
    
    // Parallele Testausführung von Unittests aktivieren
    maxParallelForks = forks
  }
  
  // Konfiguration für Unit- und Integrationstests
  tasks.withType(Test) {
    testLogging {
      exceptionFormat "full"
      events "passed", "skipped", "failed"
      minGranularity 0
    }
  }

In der zur Verfügung gestellten Beispielanwendung sind Tests in unterschiedlichen Ausprägungen umgesetzt. Einfache Persistenztests sind als Unit Tests implementiert. Hier nutzen wir das Framework Needle [3] mit HSQL als In-Memory-Datenbank. Darüber hinaus sind Tests implementiert, die EasyMock [4] zur Isolierung der zu testenden Unit verwenden. Bei beiden Ausprägungen handelt es sich um JUnit-Tests. Auf der Ebene von Gradle sind nur die Angaben der entsprechenden Dependencies notwendig.

Bei Integrationstests kommt arquillian [5] mit dem JBoss 7.1.1 im Ausführungsmodus managed zum Einsatz. In diesem Modus wird der Applikationsserver bei der Ausführung eines Tests hoch- und nach Beendigung des Tests wieder heruntergefahren. Entscheidet man sich für diesen Ausführungsmodus, so gilt zu beachten, dass man hier nicht die von Gradle bereitgestellte Möglichkeit der Parallelisierung nutzen kann, da es bei der parallelen Ausführung zweier Integrationstests zu einem Konflikt beim Hoch- und Herunterfahren kommen würde. Dies ist mit Gradle kein Problem, da die DSL eine separate Konfiguration von Unit und Integrationstests, in unserem Beispiel bezogen auf die parallele Testausführung, erlaubt. Zum anderen können aber auch gemeinsame Aspekte wie die Granularität bei der Ausgabe von Testergebnissen deklariert werden (Listing 1). Eine Herausforderung war, im Kontext der Integrationstests mit arquillian die Erstellung des zu testenden Webarchivs umzusetzen. Die Erstellung erfolgt mit dem ShrinkWrap-API [6]. Hierbei wird in der Regel nur ein Fragment der Webanwendung zusammengebaut und auf dem Applikationsserver veröffentlicht. Neben den zu testenden Komponenten müssen bei der Zusammenstellung des Testwebarchivs die benötigten Bibliotheken als Dateien angegeben werden. Die Herausforderung war hierbei, auf die von Gradle verwalteten Bibliotheken zuzugreifen. In Listing 2 zeigen wir eine einfache Lösung, in welcher mithilfe einer eigenen Task vor Ausführung der Integrationstests die für das Archiv benötigten JAR-Dateien in ein definiertes Verzeichnis kopiert werden. Anschließend können die dort abgelegten Bibliotheken im Test referenziert und das Testwebarchiv erstellt werden.

task copyLibsForArquillian(type: Copy, 
description: 'Kopiert Bibliotheken vom lokalen Gradle-Repo') {
  
  def solderApi = project.configurations.compile*.toURI().find 
{ URI uri -> uri.toString().indexOf('solder-api') >= 0 }
    
  from (new File(solderApi).getParentFile().getAbsolutePath()){
    include '**/*.jar'
  }
  into { "$buildDir/integrationTest/arquillian/libs" }
}
integrationTest.dependsOn copyLibsForArquillian

Ein weiterer wichtiger Punkt bei der Testautomatisierung ist die Ermittlung der erreichten Testabdeckung. In der Beispielanwendung haben wir uns für den Einsatz von JaCoCo [7] entschieden, einer freien Bibliothek, deren Coverage-Metriken über Plug-ins für Sonar, Jenkins und Eclipse [8] dargestellt werden können. JaCoCo stellt einen so genannten Java Agent zur Verfügung, der zur Testlaufzeit die Instrumentierung des Produktionscodes zur Messung der Testabdeckung vornimmt. Die Gradle-Anbindung wird dabei über eine separate Konfiguration realisiert, deren Dependencies als JVM-Parameter bei der Ausführung der Unit Tests übergeben werden (Listing 1).

Mittels eines so genannten ArtifactHandlers lässt sich mit Gradle auch ein weiterer wichtiger Punkt realisieren: die Erstellung von Testartefakten. Es kommt zum Beispiel regelmäßig vor, dass im Rahmen eines Projekts oder innerhalb eines Unternehmens wiederverwendbare Hilfsklassen für Tests implementiert werden. Idealerweise sollten diese Hilfsklassen vom Anwendungscode losgelöst und in einer separaten Test-JAR-Datei untergebracht sein (Listing 3).

// Deklaration eines ArtifactHandlers
configurations { testJar }

task jarTestClasses(type: Jar, description: "Test-JAR", group: 'build') {
  appendix = 'test-api'
  dependsOn classes
  from sourceSets.test.output
  
  include('com/acme/am/dao/Abstract*.class', 'com/acme/am/testsupport/*.class')
}
artifacts { testJar jarTestClasses }
    

    // Abhängigkeitsdeklaration zum Test-Artefakt
    dependencies {
      testCompile project(path: ':addressManager-dao', configuration: 'testJar')
}

[ header = Qualitätssicherung ]

Qualitätssicherung

Neben der Testautomatisierung ist die Analyse der statischen Codequalität ein wesentlicher Faktor zur Qualitätssicherung. Im Java-Umfeld existiert diesbezüglich eine Reihe etablierter Tools wie Checkstyle, PMD und Findbugs, deren Ausführung durch das Build-System unterstützt werden sollte. Gradle stellt für die drei genannten Tools eigene Plug-ins zur Verfügung, die im Rahmen des Build-Zyklus die Check-Task um spezifische Analysen des Produktionscodes erweitern. Die Konfiguration der Plug-ins haben wir in die Datei codequality.gradle ausgelagert. Eine einheitliche Deklaration ist dabei aktuell nicht möglich, da die einzelnen Plug-ins unterschiedliche Standardeinstellungen verwenden. Das Checkstyle-Plug-in setzt beispielsweise zwingend die Angabe der Konfigurationsdatei für die anzuwendenden Regeln voraus. PMD und Findbugs ermöglichen hingegen die Verwendung von Standardregelsätzen. Das PMD-Plug-in generiert zudem sowohl einen XML- als auch eine HTML-Report, Checkstyle dagegen nur XML und Findbugs genau einen der beiden. Die nahtlose Ant-Integration ermöglicht es hier, die fehlenden Reports per XSLT-Task auch in Gradle auf einfache Art und Weise zu erzeugen. Listing 4 zeigt einen Auszug aus codequality.gradle.

    task checkstyleReport(dependsOn: checkstyleMain, group: 'verification') << {
        ant.xslt(in: "$reporting.baseDir/checkstyle/main.xml", style: "...",
        out:"$reporting.baseDir/checkstyle/checkstyleMain.html") 
    }
    check.dependsOn checkstyleReport

Über die Ant-Integration lassen sich zudem Ant-Tasks verwenden, für die noch keine expliziten Gradle-Plug-ins zur Verfügung stehen. Listing 5 zeigt, wie sich beispielsweise der JavancssAntTask [9] zur Bestimmung der zyklomatischen Komplexität einbinden lässt.

    configurations { javancss }
    dependencies { javancss "javancss:javancss:$javancssVersion" }

    task javancss(group: 'verification') << {
        //...
        ant {
            taskdef(name:'javancss', classname:'javancss.JavancssAntTask',     
       classpath: configurations.javancss.asPath)
         javancss(srcdir: 'src/main/java', generateReport:'true', format: 'xml', outputfile: "$reportDir/main.xml")
      }
}

Neben der separaten Ausführung der einzelnen Analysewerkzeuge unterstützt Gradle auch die Anbindung an Sonar, eine Plattform, die die genannten Tools im Rahmen einer Webanwendung vereint und unter anderem deren Ergebnisse sowie die ermittelte Testabdeckung gebündelt darstellen kann [10]. Im Kontext einer Multi-Projekt-Struktur muss die Konfiguration des Sonar-Plug-ins dabei im Wurzelprojekt erfolgen (Listing 6). Für die separate Erfassung der Testabdeckung konfigurieren wir zudem für alle Subprojekte die Pfade der JaCoCo-Ergebnisdateien. Scheitert die Sonar-Analyse am zu hohen Arbeitsspeicherverbrauch, so können die entsprechenden JVM-Parameter in den Wrapper-Skripten angepasst werden [11].

    apply plugin: 'sonar'
    subprojects {
    sonar {
            project { withProjectProperties { props ->
                props["sonar.core.codeCoveragePlugin"] = "jacoco"
                       props["sonar.jacoco.reportPath"] =  "$buildDir/jacoco.exec" }
             }
           }
           sonarAnalyze.dependsOn check
    }
    sonar { project { dynamicAnalysis = "reuseReports" }}

[ header = Continuous Integration ]

Continuous Integration

Im Kontext professioneller Softwareentwicklung erfolgt üblicherweise eine kontinuierliche Integration (Continuous Integration, kurz CI) des seitens der beteiligten Entwickler erzeugten Produktionscodes. Die Unterstützung eines Build-Systems auf CI-Servern wie Jenkins [12] oder TeamCity [13] ist somit von zentraler Bedeutung.

Für beide genannten CI-Server existieren native Erweiterungen zur Ausführung von Gradle-Builds. Ein wesentlicher Pluspunkt im Vergleich zu anderen Build-Systemen besteht dabei in der Möglichkeit, den in der Versionsverwaltung hinterlegten Gradle-Wrapper [14] zu verwenden. Es kann somit sichergestellt werden, dass die gleiche Gradle-Version sowohl auf den Entwicklungs- als auch auf den CI-Maschinen zum Einsatz kommt. Eine manuelle Installation von Gradle durch den CI-Admin kann entfallen.

Umgebungsspezifische Konfiguration

Abschließend möchten wir noch kurz auf umgebungsspezifische Konfigurationen eingehen. Anwendungen enthalten oftmals umgebungsspezifische Einstellungen, beispielsweise für die Datenbankkonfiguration auf den unterschiedlichen Zielsystemen. Für die Durchführung von Tests kann es z. B. effektiver sein, eine In-Memory-Datenbank zu verwenden, wohingegen in der Produktionsumgebung ein dedizierter Datenbankserver zum Einsatz kommt. In der Entwicklungsumgebung ist zudem meist hilfreich, die Applikation beim Starten mit Testdaten zu initialisieren. Die Deklaration umgebungsspezifischer Parameter kann bei Gradle unter anderem mithilfe der aus Groovy bekannten ConfigSlurper-Klasse [15] realisiert werden. Vereinfacht dargestellt ermöglicht diese Klasse die Definition sowohl allgemeingültiger als auch umgebungsspezifischer Einstellungen per Groovy-Source-File. Listing 7 zeigt einen Ausschnitt einer solchen Datei (environment.groovy), die wir im Build-Skript (build.gradle) des Wurzelprojekts einlesen. Die Steuerung, für welche Umgebung der Build durchgeführt werden soll, erfolgt über den Kommandozeilenparameter env:

$ gradlew –Penv=prod war

Ist dieser nicht vorhanden, wird dev als Standard angenommen. Listing 7 zeigt auch, wie wir die Task processResources im Domain-Subprojekt nutzen, um umgebungsspezifische Platzhalter in der persistence.xml zu ersetzen.

   // umgebungsspezifische Anwendungskonfiguration in der Datei environment.groovy
   environments {
     dev {
      bootstrap = true
      hibernate { hbm2ddl { auto   = 'create-drop' } }
    }
    prod {
      bootstrap = false
      hibernate { hbm2ddl { auto   = 'validate' } }
    }
  }

   // Einlesen der Konfiguration in build.gradle des Wurzelprojekts
   def loadConfiguration() {
     def environment = hasProperty('env') ? env : 'dev'
     project.ext.set('environment', environment)  
     def configFile = file('environment.groovy')
       def config = new    ConfigSlurper(environment).parse(configFile.toURI().toURL())
    project.ext.set('config', config)
  }

   // Ersetzen von Platzhaltern in der persistence.xml (Domain-Projekt) laut 
   // Konfiguration
   processResources {
       def config = project.rootProject.ext.get('config')
       expand( hbm2ddl: "$config.hibernate.hbm2ddl.auto" )
  }

[ header = Fazit ]

Fazit

Im Rahmen von zwei praxisnahen Artikeln haben wir gezeigt, dass mit Gradle die wichtigsten Build-Anforderungen umgesetzt werden können. Neben der Ausführung einfacher Unit Tests besteht auch die Möglichkeit, Integrationstests auszuführen. Die Tool-Unterstützung für Eclipse und IDEA ist auf einem guten Niveau, Sonar und Continuous-Integration-Server wie Jenkins werden unterstützt. Der deklarative Ansatz, gepaart mit der Möglichkeit, den Build-Prozess per Groovy-DSL auf die projektspezifischen Gegebenheiten anzupassen, ermöglicht ein effizientes Build-Management. Die zu Beginn gestellte Frage, ob Gradle für den professionellen Projekteinsatz geeignet ist, lässt sich somit klar bejahen. Softwareentwicklern und -architekten steht mit Gradle ein neues und ausgereiftes Build-System zur Verfügung.

Geschrieben von
Moritz Zimmer
Moritz Zimmer
Moritz Zimmer arbeitet als Teamleiter und Senior Software Engineer bei der Axel Springer ideas Engineering GmbH. Seine Schwerpunkte liegen unter anderem im Bereich des Build- und Content Managements sowie der Testautomatisierung. Kontakt: @Moritz_Zimmer
Marek Iwaszkiewicz
Marek Iwaszkiewicz
Marek Iwaszkiewicz ist für die directline AG als Senior Software Engineer tätig. Er beschäftigt sich seit Jahren mit Java EE Architekturen und Technologien. Seine Schwerpunkte liegen unter anderem im Bereich des Buildmanagements. Kontakt: @Marek_Iwaszkiewicz
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.