Перейти к основному содержимому

Исходные наборы Gradle

· 7 мин. чтения

1. Обзор

Наборы исходных кодов дают нам мощный способ структурировать исходный код в наших проектах Gradle .

В этом кратком руководстве мы увидим, как их использовать.

2. Исходные наборы по умолчанию

Прежде чем перейти к значениям по умолчанию, давайте сначала объясним, что такое исходные наборы. Как следует из названия, исходные наборы представляют собой логическую группу исходных файлов .

Мы рассмотрим настройку проектов Java, но концепции применимы и к другим типам проектов Gradle.

2.1. Макет проекта по умолчанию

Начнем с простой структуры проекта:

source-sets 
├── src
│ ├── main
│ │ └── java
│ │ ├── SourceSetsMain.java
│ │ └── SourceSetsObject.java
│ └── test
│ └── java
│ └── SourceSetsTest.java
└── build.gradle

Теперь давайте посмотрим на build.gradle :

apply plugin : "java"
description = "Source Sets example"
test {
testLogging {
events "passed", "skipped", "failed"
}
}
dependencies {
implementation('org.apache.httpcomponents:httpclient:4.5.12')
testImplementation('junit:junit:4.12')
}

Плагин Java предполагает , что src/main/java и src/test/java являются исходными каталогами по умолчанию .

Создадим простую служебную задачу:

task printSourceSetInformation(){
doLast{
sourceSets.each { srcSet ->
println "["+srcSet.name+"]"
print "-->Source directories: "+srcSet.allJava.srcDirs+"\n"
print "-->Output directories: "+srcSet.output.classesDirs.files+"\n"
println ""
}
}
}

Здесь мы печатаем только несколько свойств исходного набора. Мы всегда можем проверить полный JavaDoc для получения дополнительной информации.

Давайте запустим его и посмотрим, что мы получим:

$ ./gradlew printSourceSetInformation

> Task :source-sets:printSourceSetInformation
[main]
-->Source directories: [.../source-sets/src/main/java]
-->Output directories: [.../source-sets/build/classes/java/main]

[test]
-->Source directories: [.../source-sets/src/test/java]
-->Output directories: [.../source-sets/build/classes/java/test]

Обратите внимание , что у нас есть два исходных набора по умолчанию: main и test .

2.2. Конфигурации по умолчанию

Плагин Java также автоматически создает для нас некоторые конфигурации Gradle по умолчанию .

Они следуют специальному соглашению об именах: <sourceSetName><configurationName> .

Мы используем их для объявления зависимостей в build.gradle :

dependencies { 
implementation('org.apache.httpcomponents:httpclient:4.5.12')
testImplementation('junit:junit:4.12')
}

Обратите внимание, что мы указываем реализацию вместо mainImplementation . Это исключение из соглашения об именах.

По умолчанию конфигурация testImplementation расширяет реализацию и наследует все ее зависимости и выходные данные .

Давайте улучшим нашу вспомогательную задачу и посмотрим, что это такое:

task printSourceSetInformation(){

doLast{
sourceSets.each { srcSet ->
println "["+srcSet.name+"]"
print "-->Source directories: "+srcSet.allJava.srcDirs+"\n"
print "-->Output directories: "+srcSet.output.classesDirs.files+"\n"
print "-->Compile classpath:\n"
srcSet.compileClasspath.files.each {
print " "+it.path+"\n"
}
println ""
}
}
}

Давайте посмотрим на вывод:

[main]
// same output as before
-->Compile classpath:
.../httpclient-4.5.12.jar
.../httpcore-4.4.13.jar
.../commons-logging-1.2.jar
.../commons-codec-1.11.jar

[test]
// same output as before
-->Compile classpath:
.../source-sets/build/classes/java/main
.../source-sets/build/resources/main
.../httpclient-4.5.12.jar
.../junit-4.12.jar
.../httpcore-4.4.13.jar
.../commons-logging-1.2.jar
.../commons-codec-1.11.jar
.../hamcrest-core-1.3.jar

Исходный набор тестов содержит выходные данные main в своем пути к классам компиляции, а также включает его зависимости.

Далее, давайте создадим наш модульный тест:

public class SourceSetsTest {

@Test
public void whenRun_ThenSuccess() {

SourceSetsObject underTest = new SourceSetsObject("lorem","ipsum");

assertThat(underTest.getUser(), is("lorem"));
assertThat(underTest.getPassword(), is("ipsum"));
}
}

Здесь мы тестируем простой POJO, который хранит два значения. Мы можем использовать его напрямую, потому что основные результаты находятся в нашем тестовом пути к классам .

Далее запустим это из Gradle:

./gradlew clean test

> Task :source-sets:test

com.foreach.test.SourceSetsTest > whenRunThenSuccess PASSED

3. Пользовательские исходные наборы

До сих пор мы видели некоторые разумные значения по умолчанию. Однако на практике нам часто нужны пользовательские наборы исходников, особенно для интеграционных тестов.

Это потому, что мы можем захотеть иметь определенные тестовые библиотеки только в пути к классам интеграционных тестов. Мы также можем захотеть выполнить их независимо от модульных тестов.

3.1. Определение пользовательских наборов источников

Давайте создадим отдельный исходный каталог для наших интеграционных тестов:

source-sets 
├── src
│ └── main
│ ├── java
│ │ ├── SourceSetsMain.java
│ │ └── SourceSetsObject.java
│ ├── test
│ │ └── SourceSetsTest.java
│ └── itest
│ └── SourceSetsITest.java
└── build.gradle

Далее настроим его в нашем build.gradle с помощью конструкции sourceSets :

sourceSets {
itest {
java {
}
}
}
dependencies {
implementation('org.apache.httpcomponents:httpclient:4.5.12')
testImplementation('junit:junit:4.12')
}
// other declarations omitted

Обратите внимание, что мы не указали какой-либо пользовательский каталог. Это потому, что наша папка соответствует имени нового исходного набора ( itest ).

Мы можем настроить, какие каталоги включены в свойство srcDirs :

sourceSets{
itest {
java {
srcDirs("src/itest")
}
}
}

Помните нашу вспомогательную задачу с самого начала? Давайте перезапустим его и посмотрим, что он напечатает:

$ ./gradlew printSourceSetInformation

> Task :source-sets:printSourceSetInformation
[itest]
-->Source directories: [.../source-sets/src/itest/java]
-->Output directories: [.../source-sets/build/classes/java/itest]
-->Compile classpath:
.../source-sets/build/classes/java/main
.../source-sets/build/resources/main

[main]
// same output as before

[test]
// same output as before

3.2. Назначение конкретных зависимостей исходного набора

Помните конфигурации по умолчанию? Теперь мы также получаем некоторые конфигурации для исходного набора itest .

Давайте используем itestImplementation для назначения новой зависимости : **

**

dependencies {
implementation('org.apache.httpcomponents:httpclient:4.5.12')
testImplementation('junit:junit:4.12')
itestImplementation('com.google.guava:guava:29.0-jre')
}

Это относится только к интеграционным тестам.

Давайте изменим наш предыдущий тест и добавим его как интеграционный тест:

public class SourceSetsItest {

@Test
public void givenImmutableList_whenRun_ThenSuccess() {

SourceSetsObject underTest = new SourceSetsObject("lorem", "ipsum");
List someStrings = ImmutableList.of("ForEach", "is", "cool");

assertThat(underTest.getUser(), is("lorem"));
assertThat(underTest.getPassword(), is("ipsum"));
assertThat(someStrings.size(), is(3));
}
}

Чтобы иметь возможность запустить его , нам нужно определить пользовательскую тестовую задачу, которая использует скомпилированные выходные данные :

// source sets declarations

// dependencies declarations

task itest(type: Test) {
description = "Run integration tests"
group = "verification"
testClassesDirs = sourceSets.itest.output.classesDirs
classpath = sourceSets.itest.runtimeClasspath
}

Эти объявления оцениваются на этапе конфигурации . Поэтому важен их порядок .

Например, мы не можем ссылаться на исходный набор itest в теле задачи до его объявления.

Давайте посмотрим, что произойдет, если мы запустим тест:

$ ./gradlew clean itest

// some compilation issues

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':source-sets:compileItestJava'.
> Compilation failed; see the compiler error output for details.

В отличие от предыдущего запуска, на этот раз мы получаем ошибку компиляции. Так что же случилось?

Этот новый исходный набор создает независимую конфигурацию.

Другими словами, itestImplementation не наследует зависимость JUnit и не получает выходные данные main .

Давайте исправим это в нашей конфигурации Gradle:

sourceSets{
itest {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
java {
}
}
}

// dependencies declaration
configurations {
itestImplementation.extendsFrom(testImplementation)
itestRuntimeOnly.extendsFrom(testRuntimeOnly)
}

Теперь давайте перезапустим наш интеграционный тест:

$ ./gradlew clean itest

> Task :source-sets:itest

com.foreach.itest.SourceSetsItest > givenImmutableList_whenRun_ThenSuccess PASSED

Тест проходит.

3.3. Обработка Eclipse IDE

До сих пор мы видели, как работать с наборами исходных текстов напрямую с Gradle. Однако в большинстве случаев мы будем использовать IDE (например, Eclipse).

Когда мы импортируем проект, мы получаем некоторые проблемы с компиляцией:

./3a6662b708ceb9c7bfdead9d80e56b80.png

Однако, если мы запустим тест интеграции из Gradle, мы не получим ошибок:

$ ./gradlew clean itest

> Task :source-sets:itest

com.foreach.itest.SourceSetsItest > givenImmutableList_whenRun_ThenSuccess PASSED

Так что же случилось? В этом случае зависимость guava принадлежит itestImplementation .

К сожалению, подключаемый модуль Eclipse Buildship Gradle не очень хорошо справляется с этими пользовательскими конфигурациями .

Давайте исправим это в нашем build.gradle :

apply plugin: "eclipse"

// previous declarations

eclipse {
classpath {
plusConfigurations+=[configurations.itestCompileClasspath]
}
}

Давайте объясним, что мы здесь сделали. Мы добавили нашу конфигурацию в путь к классам Eclipse.

Если мы обновим проект, проблемы с компиляцией исчезнут.

Однако у этого подхода есть недостаток : среда IDE не различает конфигурации.

Это означает , что мы можем легко импортировать гуаву в наши тестовые источники (чего мы специально хотели избежать).

4. Вывод

В этом уроке мы рассмотрели основы исходных кодов Gradle.

Затем мы объяснили, как работают пользовательские наборы исходных текстов и как их использовать в Eclipse.

Как обычно, мы можем найти полный исходный код на GitHub .