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:

Unmögliche Abhängigkeiten zwischen Test-Projekten

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:

Lösung über Test-Fixtures

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.

  1. 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 des test-SourceSet doch als Artefakt zu exportieren (über entsprechenden Code in der build.gradle). 

  2. Um die Benennung einfach und konsistent zu halten, empfehlen wir rootProject.name und artifactId auf denselben Wert zu setzen.