Czasem mamy przypadek, że piszemy test jednostkowy jakiejś klasy i okazuje się, że część kodu testowego może być przydatna w innych częściach naszego projektu. Jak udostępnić ten kod, żeby był widoczny zarówno w testach aktualnego projektu, jak i w testach projektów od niego zależnych? Gradle od wersji 5.6 udostępnia dla tego celu plugin java-test-fixtures.
Jak może wyglądać nasz przypadek użycia? Mamy klasę produkcyjną, na przykład Email
oraz jedną lub więcej z poniższych sytuacji:
- będziemy w testach używać predefiniowanych testowych instancji (klasa
TestEmails
ze statycznymi polami typuEmail
) - będziemy w testach używać fabryki biorącej istotne pola i uzupełniającej pola mniej ważne (klasa
TestEmailFactory
) - będziemy mieli osobne testy na renderowanie różnego typu maili, udostępnimy klasę bazową (
AbstractEmailTest
) ułatwiającą pisanie takich testów - mamy powtarzalną logikę wołaną w testach, która omija zewnętrzne systemy dostępne tylko na produkcji (
MailRenderingTestHelper
) - dostarczamy „fałszywej” implementacji naszego serwisu, żeby unikać mocków składaniających do pisania słabych testów (klasa
FakeMailSender
)
Problem pojawia się, gdy klasa Email
jest w jednym podprojekcie (załóżmy, że nazywa się core
) a inne podprojekty (na przykład podprojekt analytics
) używają jej zarówno w kodzie produkcyjnym, jak i do testowania własnych klas:
- nie możemy klas pomocniczych umieścić w testach projektu
core
, bo w Gradle’u zależność od projektu nie bierze źródeł testowych – projektanalytics
będzie widział klasęEmail
, ale na pewno nieEmailTest
i spółkę - możemy umieścić testowe klasy pomocnicze w nowym podprojekcie
core-test-utils
– to zadziała, ale jeśli klasy, które można dzielić, są nie tylko wcore
, możemy niemalże podwoić liczbę podprojektów stosując wszędzie to podejście

Spis treści
java-test-fixtures
Wygodniejszym rozwiązaniem jest zastosowanie pluginu java-test-fixtures (jest wbudowany w Gradle’a). Tworzy on trzeci source set obok main
i test
nazwany testFixtures
, gdzie będziemy umieszczać klasy, które mają być też widoczne na zewnątrz. Po dodaniu pluginu do podprojektu Idea zauważa dodatkowy source set (polecam auto-import Gradle’a) i jeśli na katalogu podprojektu wybierzemy New » Directory, Idea zasugeruje odpowiednią nazwę.

Upraszcza się konfiguracja: wcześniej musieliśmy powiązać core
z core-test-utils
i to w dwie strony. Teraz plugin załatwia zależność fixtures od głównego kodu core oraz testów core od fixtures i nie musimy tego zapisywać.
Przykład 1
Będziemy dzielić się klasą generującą testowe maile. Klasę umieszczamy w core/src/testFixtures/java/
. Source set testFixtures
ma własne zależności, odpowiednikiem implementation
jest testFixtureImplementation
, odpowiednikiem runtimeOnly
jest testFixturesRuntimeOnly
itp.
core/build.gradle
:
plugins { id 'java-library' id 'java-test-fixtures' } dependencies { implementation 'javax.mail:javax.mail-api:1.6.2' testFixturesImplementation 'com.github.javafaker:javafaker:1.0.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' }
analytics/build.gradle
:
plugins { id 'java-library' } dependencies { implementation project(':core') testImplementation testFixtures(project(':core')) testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' }
Przykład 2
Będziemy dzielić się klasą bazową dla testów. Test fixtures nie widzi zależności testowych, więc żeby móc użyć adnotacji JUnita, trzeba dodać taką zależność.
core/build.gradle
:
plugins { id 'java-library' id 'java-test-fixtures' } dependencies { implementation 'javax.mail:javax.mail-api:1.6.2' testFixturesImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' }
Kotlin
Plugin java-test-fixtures wydaje się być świetnym rozwiązaniem dla użytkowników Kotlina, w którym (niestety) jedynym narzędziem do ograniczania widoczności jest problematyczny modyfikator internal.
Wyobraźmy sobie, że mamy w podprojekcie core
klasę MailRenderer
i nie chcemy, żeby wiedziały o niej inne podprojekty — chcemy mieć nieograniczoną swobodę w zmianie implementacji. Jeśli chodzi o kod „główny”, to MailRenderer jest odpowiednio ukryty i nigdzie nie wycieka, problemem są testy innych podprojektów, bo tam też jest potrzebny. Pomysł jest taki: będziemy go wołać jedynie wewnątrz pomocniczej klasy testowej MailRenderingTestHelper
. Moglibyśmy w ten sposób zrobić klasę MailRenderer
„wewnętrzną” dla core’a, a MailRenderingTestHelper
udostępnić bez ograniczeń testom innych podprojektów.
Nie jesteśmy tego w stanie zaimplementować „tradycyjnym” podejściem. Klasa z modyfikatorem internal
jest dostępna tylko w swoim podprojekcie (czyli core
), nie będzie więc widziana w podprojekcie core-test-utils
, gdzie znajdowałby się MailRenderingTestHelper
.
Plugin java-test-fixtures powoduje, że zarówno MailRenderer
jak i MailRenderingTestHelper
umieszczamy w tym samym podprojekcie (pierwszą klasę w source secie main
, drugą w source secie testFixtures
) — wydaje się, że wszystko pięknie zadziała. Niestety, obecnie Kotlin odmówi kompilacji. Póki twórcy Kotlina nie naprawią KT-34901, nie mamy możliwości ukrywania widoczności klas wykorzystywanych we współdzielonych testowych klasach pomocniczych. Ułomność Kotlina w zarządzaniu widocznością kodu to zresztą temat, o którym długo można by pisać – może innym razem (aktualizacja: napisałem na ten temat tutaj).