Agiles Testen von JIRA Plugins

Keine Kommentare

Für die JIRA Plugin-Entwickung existiert eine beeindruckende Infrastruktur, und normalerweise finden Entwickler die Antworten auf die meisten ihrer Fragen. Das Ganze sieht etwas anders aus, wenn es um das agile, d.h. automatisierte und durch Entwickler getriebene, Testen geht. Mit dieser Artikelserie möchten wir – das sind Raimar Falke und ich – Entwicklern, die die ersten Erfahrungen mit der Plugin-Entwicklung für JIRA sammeln, mit den Möglichkeiten des agilen Testens von Plugins vertraut machen, ihnen helfen, die richtigen Testarten auszuwählen und die notwendigen Schritte bis zum laufenden Test zu verstehen. In diesem ersten Artikel möchten wir zunächst eine Einführung in die Thematik geben, die verschiedenen Testarten im Kontext von JIRA im Allgemeinen vorstellen und abschließend Unittests als erste Art genauer unter die Lupe nehmen.

Hinweis: Dies ist eine 4-teilige blog Serie. Agiles Testen von JIRA Plugins (Teil 1), Wired Tests (Teil 2), Systemtests (Teil 3), CI Server Integration und Test Coverage (Teil 4). Darüber hinaus finden Sie auch eine Einführung zum Thema JIRA mit Plugins erweitern in unserem blog.

JIRA und Plugins

JIRA ist ein Projektverfolgungstool der Firma Atlassian, welches sich durch umfangreiche Funktionen und höchste Anpassbarkeit auszeichnet. Es wird weltweit von einer großen Zahl an Unternehmen, Organisationen und Projektteams eingesetzt. Plugins, manchmal auch als Add-Ons bezeichnet, sind eine Möglichkeit, JIRA noch stärker anzupassen oder zu erweitern. Auch wenn Atlassian bereits eine Vielzahl an Plugins auf dem Marktplatz bereitstellt, kann es Situationen geben, in denen eine maßgeschneiderte Eigenentwicklung die einzige Lösung ist. Glücklicherweise stellt Atlassian ein SDK zur Entwicklung eigener Erweiterungen für ihre Produkte, sowie ein breites Spektrum an Dokumentation und eine Community-getriebene Fragen-und-Antworten-Seite bereit. Ein zentraler Bestandteil jedes Softwareprojekts – und die Erstellung eines JIRA Plugins ist ein Projekt – ist der Test der Software. Doch obwohl Atlassian Dokumentation und Beispiele für die meisten Testszenarien bereitstellt, ist nicht immer klar, welche Technologie oder Methode genutzt werden kann und sollte (oder auch nicht) vor allem, da sich die Tests und Testerstellung so nahtlos wie möglich in die üblichen Entwicklungsprozesse integrieren sollten.

Relevante und verwandte Technologien und Begrifflichkeiten

Atlassian Produkte im Allgemeinen und JIRA im Speziellen setzen eine Reihe von Technologien ein, die nicht unbedingt allgemein bekannt sind. Daher werden wir im Folgenden kurz auf diejenigen eingehen, die wir im Kontext dieser Artikel für relevant halten.

Maven

Maven ist das Buildmanagementwerkzeug für alle Atlassian Produkte und deren Erweiterungen. Es ist in der Lage, extrem modulare Projekte inklusive ihrer internen und externen Abhängigkeiten, den Buildprozess und die Erstellung von Reports zu verwalten und kann auf einfachste Weise in Continuous Integration (CI) Systeme integriert werden. Atlassian stellt Wrapper für eine Vielzahl an Maven Kommandos bereit, um übliche Entwicklungsschritte zu vereinfachen (siehe Atlassian Maven Befehle).

OSGi

OSGi ist sowohl ein Konsortium als auch eine Spezifikation für modulare Java Softwaresysteme. Atlassian, wie auch z.B. Eclipse, benutzt einen OSGi Container als Grundlage für alle ihre Produkte, und alle Plugins sind im Prinzip OSGi Bundles. Daraus ergeben sich gewisse Einschränkungen und Best Practises, die man während der Entwicklung – und fast noch wichtiger: beim Testen – berücksichtigen muss. Wenn wir im Folgenden von einem Container sprechen, beziehen wir uns i.d.R. auf den OSGi Container.

Host Application

Die Anwendung, z.B. JIRA oder Confluence , in der das zu entwickelnde Plugin eingesetzt wird.

Active Objects

Active Objects ist eine ORM Schicht für Atlassian Produkte. Da es das empfohlene Verfahren für das Speichern und den Zugriff auf Daten ist, spielt es beim Test auch eine Rolle.

FastDev

Der Test (automatisiert oder auch manuell) eines Plugins, welches in einem Container läuft, z.B. für Oberflächentests, ist mühselig, wenn Container und JIRA immer wieder neu gestartet werden müssen, und anschließend das Plugin in der aktuellen Version installiert wird. Da allein der Start von JIRA ca. 3 Minuten dauert, kann sich die Wartezeit schnell zu einem beträchtlichen Teil des Tages aufsummieren, auch wenn jedes Mal nur kleine Anpassungen am Plugin vorgenommen wurden. FastDev (was selbst ein Plugin ist) stellt eine Möglichkeit zur Verfügung, sowohl Änderungen am Quellcode des Plugins aus dem laufenden JIRA heraus zu erkennen als auch das Plugin anschließend neu zu bauen und zu laden, ohne die Host Application neu starten müssen, was den Entwicklungszyklus erheblich beschleunigt.

Atlassian Maven Befehle

Die folgende Tabelle stellt einen Teil der Atlassian Befehle für typische Entwicklungsaufgaben sowie ihre entsprechenden Maven Befehle vor.

BefehlMaven EntsprechungBeschreibung
atlas-cleanmvn cleanRäumt das Projekt auf (d.h. löscht den target-Ordner).
atlas-unit-testmvn testBaut das Projekt und führt die Unittests aus.
atlas-integration-testmvn integration-testBaut das Projekt, führt die Unittests aus, startet eine JIRA Instanz, installiert das Plugin und führt Integrationstests in der/gegen die Instanz aus.
atlas-runmvn jira:runBaut das Projekt, führt die Unittests aus, startet eine JIRA Instanz und installiert das Plugin. Sinnvoll, um eine laufende Instanz während der Entwicklung wiederzuverwenden, um bspw. Start- und Stopzeiten zu sparen. Um nicht die aktuellste, sondern eine spezifische Version zu starten, kann man die Version als Parameter übergeben.
atlas-debugmvn jira:debugBaut das Projekt, führt die Unittests aus, startet eine JIRA Instanz und installiert das Plugin. Zusätzlich zum run-Befehl wird hier noch ein Port zum Remote-Debugging geöffnet.
atlas-install-pluginmvn jira:installInstalliert das Plugin in eine laufende JIRA Instanz. Das Plugin muss vorher gebaut worden sein.
atlas-remote-testmvn jira:remote-testBaut das Projekt, führt die Unittests aus, installiert das Plugin in eine laufende JIRA Instanz und führt Integrationstests in der/gegen die Instanz aus.
atlas-updatemvn amps:updateAktualisiert das SDK.

Einrichten der Infrastruktur

Atlassian Produkte sind im Prinzip Java (Web-)Anwendungen, die mittels Maven gebaut werden. Bei der Installation des SDK wird eine eigene Maven Distribution, eine settings.xml, ein lokales Maven Repository und eine Reihe von Shell-Skripten (die oben erwähnten Atlassian Befehle) eingerichtet. Ein JDK hingegen wird als installiert vorausgesetzt. Unsere Versuche haben ergeben, dass JIRA bis zur Version 6.2.6 nicht startet, wenn man ein JDK 8 verwendet. Wir empfehlen daher, ein JDK 7 zu verwenden, da es ein Problem mit Type Inference behebt, welches mit JDK 6 besteht und das zumindest den Umgang mit Hamcrest erschwert. Auch wenn ein JDK 7 eingesetzt wird, müssen Source- und Bytecode auf Version 1.6 gesetzt werden. Es wird zwar nirgendwo explizit erklärt, man findet es aber in den meisten Beispielen von Atlassian. Während der Arbeit an unserem Projekt wurde die aktuelle Version des SDK (zu dem Zeitpunkt: 4.2.20) noch mit Maven 2.1.0 ausgeliefert, was leider zu Problemen mit einigen Maven Plugins, die wir aber als ziemlich wichtig erachten, führt. So benötigt z.B. FindBugs Maven in einer Version von mindestens 2.2.1 und auch Sonar benötigt mindestens Maven 2.2. Zum Glück gibt es aber mindestens zwei Wege, wie man das SDK dazu bringen kann, mit einer neueren Maven Version zu arbeiten.

  • Setzen der Umgebungsvariable ATLAS_MVN (eine Beschreibung findet sich hier)
  • Der Wert der Variablen muss auf die ausführbare Datei der Maven Installation (unter Windows mvn.bat) zeigen. Alle atlas-* Befehle nutzen dann diese Variable anstelle der Mitgebrachten, um die eigentlichen Kommandos auszuführen, sodass effektiv die alternative Maven Installation genutzt wird. Der Nachteil dieses Vorgehens ist, dass man immer noch die atlas-* Befehle nutzen muss, was von einigen Tools nicht unterstützt wird.
  • Kopieren der settings.xml, die mit dem SDK kam, in die gewünschte Maven Installation oder die Benutzereinstellungen
  • Dieses Vorgehen löst eine Reihe von Problemen, inklusive einigen Buildproblemen mit FastDev. Der größte Vorteil ist die Möglichkeit, ab sofort „Standard“ Maven Kommandos zu benutzen, also bspw. „mvn clean“ statt „atlas-clean“, was die Integration mit anderen Tools oft vereinfacht. So ist bspw. die Integration in die meisten IDEs so ein Kinderspiel. Es muss jedoch beachtet werden, dass bei der Übernahme der settings.xml eine ggfs. bestehende Konfiguration nicht einfach gelöscht wird, und alle Änderungen, die das SDK erfordert, müssen von Hand nachgetragen werden. Ein weiterer Nachteil ist, dass diese globale Anpassung auch andere Projekte beeinflussen kann, auch wenn es sich nicht um JIRA Plugins handelt. Eine mögliche Alternative ist die Bereitstellung verschiedener Maven Installationen (für die gleiche Maven Version), wobei in einer die Änderungen für Atlassian integriert werden und in der anderen nicht. Auf diese Weise kann man durch Setzen des Pfades (PATH Umgebungsvariable) zwischen den Installationen wechseln.

Es gibt jedoch Einschränkungen, welche alternativen Maven Versionen eingesetzt werden können. Unsere Versuche haben ergeben, dass sowohl Versionen 3.1.* als auch 3.2.* nicht einsetzbar sind, da es eine inkompatible Änderung in der API des Felix Plugins gab, welches vom Atlassian SDK benötigt wird. Eine typische Fehlermeldung für einen derartigen Fall war beispielsweise:

[ERROR] Failed to execute goal com.atlassian.maven.plugins:maven-jira-plugin:4.2.20:copy-bundled-dependencies (default-copy-bundled-dependencies) on project test: Execution default-copy-bundled-dependencies of goal com.atlassian.maven.plugins:maven-jira-plugin:4.2.20:copy-bundled-dependencies failed: An API incompatibility was encountered while executing com.atlassian.maven.plugins:maven-jira-plugin:4.2.20:copy-bundled-dependencies: java.lang.NoSuchMethodError: org.apache.maven.execution.MavenSession.getRepositorySession()Lorg/sonatype/aether/RepositorySystemSession;
[ERROR] -----------------------------------------------------
[ERROR] realm = plugin>com.atlassian.maven.plugins:maven-jira-plugin:4.2.20
[ERROR] strategy = org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy
[ERROR] urls[0] = file:/C:/user/.m2/repository/com/atlassian/maven/plugins/maven-jira-plugin/4.2.20/maven-jira-plugin-4.2.20.jar

Maven 3.0.* funktioniert hingegen ohne Probleme, weshalb wir den Einsatz auch empfehlen.

Wie Entwickler testen wollen

Es gibt zwei typische Wege, wie Tests durchgeführt werden: während der Entwicklung und im Rahmen der Continuous Integration. Ersteres hilft dem Entwickler, seine eigenen Anpassungen im (testgetriebenen) Prozess zu testen, während der zweite Weg dazu dient sicherzustellen, dass die Anpassungen keine Auswirkungen auf andere Funktionalitäten hatten. Während kurze Ausführungszeiten generell wichtig sind, werden Tests aus der IDE heraus öfter ausgeführt, sodass die Geschwindigkeit hier noch kritischer ist. Außerdem ist es bei der Ausführung aus der IDE wichtig, genau auswählen zu können, welche Tests – eingeschränkt auf einzelne Klassen oder sogar Methoden – auszuführen sind, um Zeit zu sparen. Für die Ausführung auf einem CI Server ist es hingegen wichtig, dass die Tests stabil laufen (können auf allen Build-Agents laufen, keine Wackeltests, die den Build abbrechen lassen, o.ä.) und dass sie reproduzierbar sind, d.h. der Kontext (Betriebssystem, Leistungsfähigkeit, andere Softwareinfrastruktur) ist vorgegeben und kann bei Bedarf wiederhergestellt werden. Darüber hinaus werden auf einem CI Server die Tests i.d.R. zusammen, alle nacheinander oder parallel, ausgeführt, während in der IDE Tests einzeln bzw. sequentiell gestartet werden.

Bei der Frage, was für Tests eingesetzt werden sollten, gibt die Test Pyramide üblicherweise die Empfehlung für drei Arten von Tests:

  1. Unittests sind dazu da, eine Komponente (das Testsubjekt) isoliert zu testen. Dazu wird die Interaktion mit anderen Komponenten (Abhängigkeiten) durch den Test gesteuert. Am besten eignen sich dazu „Mocks“, die die Schnittstelle und das Verhalten einer Abhängigkeit nachbilden. Es gibt viele Gründe, die für den Einsatz von Mocks sprechen: sie erlauben eine exakte Kontrolle des Verhaltens und ermöglichen es, auch ungewöhnliche Situationen zu simulieren. Außerdem erlauben sie die Entkopplung von externen Ressourcen, wie beispielsweise Netzwerken, Datenbanken oder dem Dateisystem, die teilweise zu langsam für einen häufig ausgeführten Test oder schwierig in einen definierten Zustand zu versetzen sind.
  2. Servicetests sind End-to-End Tests ohne die zusätzliche Komplexität der Benutzerschnittstelle.
  3. Oberflächentests testen zusätzlich noch die Benutzeroberfläche.

JIRA Plugins bestehen häufig aus JavaScript, welches im Browser läuft, und mit dem Java Teil des Plugins auf dem Server mittels REST kommuniziert. Servicetests sind daher größtenteils REST Tests. Oberflächentests testen dann zusätzlich auch das JavaScript und den HTML Code.

Verfügbare Test in JIRA

Die folgende Tabelle stellt die Arten von Tests für JIRA Plugins vor, die wir identifiziert haben. Ein besonderer Aspekt, den wir betrachtet haben, ist die Frage, wo Testcode und Testsubjekt ausgeführt werden. Normalerweise wird der Test in der Original-VM (durch die IDE oder den CI Server gestartet) ausgeführt. Im Rahmen der Tests von JIRA Plugins gibt es jedoch auch auch eine Art von Tests, die direkt in der Host Application durchgeführt werden. Die gleiche Unterscheidung trifft auch bei Oberflächentests mit Selenium zu: der Testcode läuft in der lokalen VM, während das Testsubjekt in einer anderen VM ausgeführt wird.

TesttypTestcode läuft inTestsubjekt läuft in
UnittestOriginal VMOriginal VM
IntegrationstestOriginal VMOriginal VM
„Traditioneller Integrationtest“ (nach Atlassian)

  • Servicetest
  • Oberflächentest
Original VMHost Application
„Wired Tests“ (nach Atlassian)Host ApplicationHost Application

Unittest

Bei der Entwicklung von Unittests wird von Atlassian empfohlen, diese in Packages mit dem Präfix ut.* (ut steht für Unittests) abzulegen. Dies ist nicht absolut notwendig, dient aber dazu, sie von Integrationstests (für die es notwendig ist, sie in einem Package mit Präfix it.* für Integrationstests abzulegen) und normalen Hilfsklassen zu unterscheiden. Wie bereits oben erwähnt, dienen Unittests dazu, einen Aspekt des Systems isoliert zu testen. Um diese Isolation zu erreichen, ist es notwendig, entweder extrem lose gekoppelte und unabhängige Komponenten zu entwickeln, oder Mock Frameworks einzusetzen. Da die lose Kopplung hier nicht nicht im Vordergrund steht, demonstrieren wir im Folgenden den Einsatz von Mock Frameworks.

Abhängigkeiten

Um Unittests zu erstellen, müssen mindestens die folgenden Abhängigkeiten eingebunden werden. Unter anderem werden so eine Menge Mockobjekte, die von Atlassian bereitgestellt werden, sowie mockito eingebunden.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.jira</groupId>
    <artifactId>jira-tests</artifactId>
    <version>${jira.version}</version>
    <scope>provided</scope>
</dependency>

Erzeugung von Mocks

Im Rahmen der Unittests können Mocks auf die übliche Weise erzeugt werden (die statischen Imports stammen aus Mockito):

MutableIssue issue = mock(MutableIssue.class);
Project project = mock(Project.class);
when(issue.getProjectObject()).thenReturn(project);
when(issueManager.getIssueObject(1)).thenReturn(issue);

Eine Besonderheit von OSGi ist die Umsetzung von Dependency Injection durch den Komponentenkonstruktur. Der Effekt davon ist, dass die meisten JIRA Komponenten Konstruktoren mit vielen Parametern haben. Um derartige Komponenten zu testen, müssen natürlich alle diese Abhängigkeiten gemockt werden (FooBar ist die zu testende Komponente):

I18nHelper i18nHelper = mock(I18nHelper.class);
PermissionManager permissionManager = mock(PermissionManager.class);
IssueManager issueManager = mock(IssueManager.class);
FooBar foo = new FooBar(i18nHelper, permissionManager, issueManager);

Eine Alternative zu dieser Art von Dependency Injection ist der Einsatz des ComponentAccessor. Doch auch wenn es scheinbar hilft, die Komponente aufzuräumen und unnötige Parameter zu entfernen, hat es einige Nachteile, vor allem was die Testbarkeit betrifft. So ist während eines Unittests das System eigentlich gar nicht komplett gestartet und der ComponentAccessor nicht initialisiert, was zu entsprechenden Fehlermeldungen führt. Eine Lösung hierfür stellt der Einsatz des MockComponentWorker dar, welcher dem ComponentAccessor die benötigten Komponenten liefert (die verwendeten Mocks sind die gleichen wie im vorigen Beispiel):

new MockComponentWorker()
    .addMock(PermissionManager.class, permissionManager)
    .addMock(I18nHelper.class, i18nHelper)
    .addMock(IssueManager.class, issueManager).init();

Trotz dieser Möglichkeit empfehlen wir jedoch, die Dependency Injection über den Konstruktor zu wählen und nicht den ComponentAccessor/MockComponentWorker Ansatz zu verwenden. Ein Vorteil ist, dass man so an einer Stelle alle Abhängigkeiten einer Komponente sieht. Ansonsten muss man den gesamten Code nach Referenzen auf den ComponentAccessor durchsuchen oder durch herumprobieren herausfinden, welche Komponenten benutzt werden.

Test von Active Objects

Um Persistenzmechanismen mit Active Objects zu testen, benötigt man weitere Abhängigkeiten im Projekt. Um die Versionen zwischen dem System und den Testartefakten konsistent zu halten, empfehlen wir den Einsatz einer Maven Property (hier: ao.version), die man dann ebenso bei der Definition der Abhängigkeit auf Active Objects selbst einsetzt:

<dependency>
    <groupId>net.java.dev.activeobjects</groupId>
    <artifactId>activeobjects-test</artifactId>
    <version>${ao.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.activeobjects</groupId>
    <artifactId>activeobjects-test</artifactId>
    <version>${ao.version}</version>
    <scope>test</scope>
</dependency>

Die Tests selbst sind dann normale JUnit Tests, denen einige spezifische Annotationen hinzugefügt werden (siehe auch das Beispiel weiter unten):

  1. Active Objects Tests benötigen einen speziellen Testrunner.
  2. Es muss angegeben werden, welche (Art von) Datenbank zum Test verwendet werden soll.
  3. Eine Klasse, die die Datenbank vorbereitet, muss angegeben werden.

Für den letzten Punkt ist es notwendig, eine Implementierung des Interface DatabaseUpdater zu erstellen. Laut Dokumentation wird diese Implementierung einmal pro Klasse aufgerufen (sie kann aber auch für mehrere Klassen genutzt werden, wenn sie öffentlich ist). In ihrer update Methode muss sie dem Entity Manager anweisen, alle für den Test relevanten Entitäten zu migrieren, d.h. die Datenbank für ihre Nutzung vorzubereiten:

public class TestDatabaseUpdater implements DatabaseUpdater {
 
    @Override
    public void update(EntityManager entityManager) throws Exception {
        entityManager.migrate(Foo.class, Bar.class);
    }
}

Active Objects unterstützt (auch im Test) ein breites Spektrum an Datenbankservern, z.B. HSQL (als In-Memory und Dateisystem Variante), MySQL, Postgres und Derby. Standardmäßig wird jeder Test in seiner eigenen Transaktion durchgeführt, die anschließend zurückgerollt wird. Das funktioniert jedoch nur, wenn die zu testende Klasse die Transaktionsverwaltung dem Container überlässt (wie in der zweiten Hälfte dieses Artikels beschrieben). Hat man sich jedoch für die Variante entschieden, die in der ersten Hälfte des Artikels beschrieben ist, übernimmt also die Komponente die Transaktionsverwaltung, muss jeder Test mit @NonTransactional annotiert werden. Das folgende Beispiel ist ein solcher Test (der den oben gezeigten DatabaseUpdater verwendet):

@RunWith(ActiveObjectsJUnitRunner.class)
@Data(TestDatabaseUpdater.class)
@Jdbc(Hsql.class)
public class FooRepositoryTest {
 
    // gets injected by the ActiveObjectsJUnitRunner
    private EntityManager entityManager;
 
    // AO repository under test
    private FooRepository fooRepository;
 
    @Before
    public void setup() {
        this.fooRepository = new FooRepositoryImpl(new TestActiveObjects(entityManager));
    }
 
    @Test
    @NonTransactional
    public void test_that_saved_value_can_be_retrieved() {
        Foo foo = new Foo("test");
        this.fooRepository.save(foo);
        List<Foo> foos = this.fooRepository.findAll();
        assertThat(foos, hasItem(
            Matchers.<Foo> hasProperty("name", is("test"))));
    }
}

Ausführen von Unittests

Der von Atlassian vorgesehene Weg, um Unittests auszuführen ist das Kommando „atlas-unit-test“. Hat man aber die Umgebung wie oben beschrieben eingerichtet, können Unittests auch mit dem Kommando „mvn test“ oder aus der IDE heraus mit dem Unittest-Starter ausgeführt werden.

Zusammengefasst

Es gibt einige Stolpersteine auf dem Weg zum erfolgreichen Test eines JIRA Plugins. Die ersten sollte man mit Hilfe dieses Artikels bewältigen können. Hat man die Umgebung erst einmal passend eingerichtet, ist die Implementierung und die Ausführung von Unittests vergleichsweise einfach. Im nächsten Artikel werden wir uns „Wired Tests“ – was sie sind und wie sie dem Entwickler helfen können – genauer anschauen.

Andere Teile dieser Serie

Agiles Testen von JIRA Plugins (Teil 2): Wired Tests

Agiles Testen von JIRA Plugins (Teil 3): Systemtests

Thomas Strecker

Thomas Strecker ist als Senior IT Consultant bei der codecentric AG tätig und leitet mit Marcel Wolf den Standort Berlin. Neben seiner Führungsarbeit beschäftigt er sich in Projekten weiterhin mit Themen wie Java Profiling und Performance-Analyse, Microservices oder auch Testautomatisierung im Kontext agiler Softwareentwicklung.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.