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
TestEmailsze 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 – projektanalyticsbędzie widział klasęEmail, ale na pewno nieEmailTesti 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).