Test fixtures are helper classes and resources upon which tests are built. For instance:

  • Initializing a model class with test data
  • Helper methods for json manipulation and evaluation
  • widely used mocking constructions

Test fixtures are often created by extracting code that is used in several test classes. It becomes interesting if this is to be done across several (sub-)projects.

The Problem: Neither test, nor production code

In this example we have an application with three subprojects: app, domain and exporters:

Unmögliche Abhängigkeiten zwischen Test-Projekten

The tests in the domain project require concrete test data. These are built in a separate TestDataFactory class. This factory would also provide a useful basis to test the json exporter in the exporters project. Unfortunately, a dependency cannot be directly defined. 1 This limitation is a deliberate design decision in Gradle, as tests are usually only executed and do not provide artifacts for other projects.

The solution: The java-test-fixtures plugin

With Gradle 5.6, test fixtures are natively supported:

Lösung über Test-Fixtures

For this purpose, the corresponding Gradle plugin is integrated into the subproject that provides the test fixtures:

domain/build.gradle:

plugins {
    id 'java-library'
    id 'java-test-fixtures'
}

dependencies {
    // ...
    testFixturesImplementation 'com.google.guava:guava:31.1-jre'
}

Gradle then automatically creates and configures a new SourceSet testFixtures, in addition to main and test. The default directory structure is as follows:

︙
├── 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
    ︙

Test fixture dependencies can be specified using the testFixturesImplementation and testFixturesApi configurations. These behave like the implementation and api configurations of the java-library plugin.

Including the test fixtures as a dependency

In the last step, the dependency in the exporters project is declared:

exporters/build.gradle:

dependencies {
    // ...
    testImplementation testFixtures(project(':domain'))
}

With this setup, the ExportTest class from the exporters project can now access the TestDataFactory from the domain project.

Separate projects

Sharing the test fixtures works not only between Gradle subprojects, but also with separate projects.

In such cases, dependencies are established through published artifacts. With the java-test-fixtures plugin no further configuration is necessary. By default, another artifact is already created and published:

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

With Gradle’s own maven-publish plugin, additional meta-information is stored in the *.module file. 2

This allows the dependencies to be declared in the second project in the expected format:

dependencies {
    // ...
    testImplementation testFixtures("mygroup.myproject:1.0")
}

IDE Support

Since the Gradle plugin is based on existing techniques (SourceSet), the IDE support is good. When searching for text and symbols, the IDEs usually provide filtering capabilities. You can limit the search results to test or production code. The test fixtures are classified differently:

  • For Eclipse, test fixtures are (correctly) part of the test code.
  • In IntelliJ, the test fixtures are considered production code. If you want to change the assignment during filtering, you can set up your own scopes.

  1. In Gradle up to version 5.5.1 several Workarounds were used: (1) a separate project for the test fixtures (domain-test-common); (2) Get Gradle to export the compiled classes of the test-SourceSet as artifact (via appropriate code in the build.gradle). 

  2. To keep naming simple and consistent, we recommend setting rootProject.name and artifactId to the same value.