Test-Fixtures sind Hilfsklassen und Ressourcen, auf denen Tests aufbauen. Beispielsweise
- die Initialisierung einer Model-Klasse mit Testdaten
- Hilfsmethoden zur json-Manipulation und Auswertung
- vielfach genutzte Mocking-Konstruktionen
Häufig entstehen Test-Fixtures durch Extraktion von Code, der in mehreren Test-Klassen zum Einsatz kommt. Interessant wird es, wenn dies über mehrere (Sub-)Projekte hinweg geschehen soll.
Das Problem: Weder Test- noch Produktionscode
Etwas konkreter soll hier eine Applikation mit drei Subprojekten als Beispiel dienen: app
, domain
und exporters
:
Die Tests im domain
-Projekt benötigen konkrete Testdaten. Diese werden in einer separaten Klasse TestDataFactory
aufgebaut.
Um den json-Exporter im exporters
-Projekt zu testen, würde diese Factory auch eine nützliche Grundlage bilden.
Aber eine Abhängigkeit kann nicht direkt definiert werden. 1 Diese Einschränkung ist eine bewusste Design-Entscheidung in
Gradle, da Tests im Normalfall nur ausgeführt werden, aber selbst keine Artefakte für andere Projekte bereitstellen.
Die Lösung: Das java-test-fixtures Plugin
Mit Gradle 5.6 werden Test-Fixtures nativ unterstützt:
Dazu wird in dem Projekt, welches Test-Fixtures bereitstellt, das entsprechende Gradle-Plugin eingebunden:
domain/build.gradle
:
plugins {
id 'java-library'
id 'java-test-fixtures'
}
dependencies {
// ...
testFixturesImplementation 'com.google.guava:guava:31.1-jre'
}
Gradle erzeugt und konfiguriert dann automatisch ein neues SourceSet testFixtures
, neben main
und test
.
Die Verzeichnisstruktur sieht mit der Standardkonvention wie folgt aus:
︙ ├── domain │ ├── build.gradle │ └── src │ ├── main │ │ └── java │ │ └── de/cronn │ │ └── CustomerEntity.java │ ├── test │ │ └── java │ │ └── de/cronn │ │ └── ModelTest.java │ └── testFixtures │ └── java │ └── de/cronn │ └── TestDataFactory.java └── exporters ├── build.gradle ︙
Abhängigkeiten der Test-Fixtures lassen sich über die bereitgestellten Konfigurationen testFixturesImplementation
und testFixturesApi
angeben. Diese verhalten sich wie die implementation
und api
Konfigurationen
des java-library
-Plugins.
Einbinden der Test-Fixtures als Abhängigkeit
Im letzten Schritt wird noch die Abhängigkeit im exporters
-Projekt deklariert:
exporters/build.gradle
:
dependencies {
// ...
testImplementation testFixtures(project(':domain'))
}
Mit diesem Setup kann die Klasse ExportTest
aus dem exporters
-Projekt nun auf die TestDataFactory
aus dem domain
-Projekt zugreifen.
Separate Projekte
Das Teilen der Test-Fixtures funktioniert nicht nur zwischen Gradle-Subprojekten, sondern auch bei separaten Projekten.
Abhängigkeiten werden dann über veröffentlichte Artefakte hergestellt. Mit dem java-test-fixtures Plugin ist keine weitere Konfiguration nötig. Es wird standardmäßig bereits ein weiteres Artefakt erzeugt und veröffentlicht:
mygroup ├── myproject │ └── 1.0 │ ├── myproject-1.0-plain.jar │ ├── myproject-1.0-sources.jar │ ├── myproject-1.0-test-fixtures.jar │ ├── myproject-1.0.module │ └── myproject-1.0.pom
Mit dem Gradle-eigenen maven-publish Plugin werden zusätzliche Meta-Information im *.module
-File abgelegt. 2
Damit lassen sich in dem zweiten Projekt die Abhängigkeiten sauber deklarieren:
dependencies {
// ...
testImplementation testFixtures("mygroup.myproject:1.0")
}
IDE-Unterstützung
Da das Gradle-Plugin auf bestehenden Techniken aufsetzt (SourceSet
), ist die Unterstützung in den IDEs gut.
Bei der Suche nach Text und Symbolen bieten die IDEs in der Regel Filterfunktionen. So kann man die Suchergebnisse auf Test- oder Produktionscode einschränken. Die Test-Fixtures werden dabei unterschiedlich eingeordnet:
- Für Eclipse zählen Test-Fixtures korrekterweise zum Testcode.
- In IntelliJ werden die Test-Fixtures als Produktionscode angesehen. Wer die Zuordnung beim Filtern ändern möchte, kann sich eigene Scopes einrichten.
Links
-
In Gradle bis Version 5.5.1 hat man sich verschiedener Workarounds bedient: (1) ein separates Projekt für die Test-Fixtures (
domain-test-common
); (2) Gradle dazu bringen, die kompilierten Klassen destest
-SourceSet doch als Artefakt zu exportieren (über entsprechenden Code in derbuild.gradle
). ↩ -
Um die Benennung einfach und konsistent zu halten, empfehlen wir
rootProject.name
undartifactId
auf denselben Wert zu setzen. ↩