1. Overview

Source sets give us a powerful way to structure source code in our Gradle projects.

In this quick tutorial, we’re going to see how to use them.

2. Default Source Sets

Before jumping into the defaults, let’s first explain what source sets are. As the name implies, source sets represent a logical grouping of source files.

We’ll cover the configuration of Java projects, but the concepts are also applicable to other Gradle project types.

2.1. Default Project Layout

Let’s start with a simple project structure:

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

Now let’s take a look at the 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')
}

The Java plugin assumes src/main/java and src/test/java as default source directories. 

Let’s craft a simple utility task:

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 ""
        }
    }
}

We’re printing just a few source set properties here. We can always check the full JavaDoc for more information.

Let’s run it and see what we get:

$ ./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]

Notice we have two default source sets: main and test.

2.2. Default Configurations

The Java plugin also automatically creates some default Gradle configurations for us.

They follow a special naming convention: .

We use them to declare the dependencies in build.gradle:

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

Notice that we specify implementation instead of mainImplementation. This is an exception to the naming convention.

By default, testImplementation configuration extends implementation and inherits all its dependencies and outputs.

Let’s improve our helper task and see what this is about:

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 ""
        }
    }
}

Let’s take a look at the output:

[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

The test source set contains the outputs of main in its compile classpath and also includes its dependencies.

Next, let’s create our unit test:

public class SourceSetsTest {

    @Test
    public void whenRun_ThenSuccess() {
        
        SourceSetsObject underTest = new SourceSetsObject("lorem","ipsum");
        
        assertThat(underTest.getUser(), is("lorem"));
        assertThat(underTest.getPassword(), is("ipsum"));
    }
}

Here we test a simple POJO that stores two values. We can use it directly because the main outputs are in our test classpath.

Next, let’s run this from Gradle:

./gradlew clean test

> Task :source-sets:test

com.baeldung.test.SourceSetsTest > whenRunThenSuccess PASSED

3. Custom Source Sets

So far, we’ve seen some sensible defaults. However, in practice, we often need custom source sets, especially for integration tests.

That’s because we might want to have specific test libraries only on the integration tests classpath. We also might want to execute them independently of unit tests.

3.1. Defining Custom Source Sets

Let’s craft a separate source directory for our integration tests:

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

Next, let’s configure it in our build.gradle using the sourceSets construct:

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

Notice we did not specify any custom directory. That’s because our folder matches the name of the new source set (itest).

We can customize what directories are included with the srcDirs property:

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

Remember our helper task from the beginning? Let’s rerun it and see what it prints:

$ ./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. Assigning Source Set Specific Dependencies

Remember default configurations? We now get some configurations for the itest source set as well.

Let’s use itestImplementation to assign a new dependency:

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

This one only applies to integration tests.

Let’s modify our previous test and add it as an integration test:

public class SourceSetsItest {

    @Test
    public void givenImmutableList_whenRun_ThenSuccess() {

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

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

To be able to run it, we need to define a custom test task that uses the compiled outputs:

// source sets declarations

// dependencies declarations 

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

These declarations are evaluated during the configuration phase. As a result, their order is important.

For example, we cannot reference the itest source set in the task body before this is declared.

Let’s see what happens if we run the test:

$ ./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.

Unlike the previous run, we get a compilation error this time. So what happened?

This new source set creates an independent configuration.

In other words, itestImplementation* does not inherit the JUnit dependency, nor does it get the outputs of *main.

Let’s fix this in our Gradle configuration:

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

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

Now let’s rerun our integration test:

$ ./gradlew clean itest

> Task :source-sets:itest

com.baeldung.itest.SourceSetsItest > givenImmutableList_whenRun_ThenSuccess PASSED

The test passes.

3.3. Eclipse IDE Handling

We’ve seen so far how to work with source sets directly with Gradle. However, most of the time, we’ll be using an IDE (such as Eclipse).

When we import the project, we get some compilation issues:

compilation issue eclipse

However, if we run the integrations test from Gradle, we get no errors:

$ ./gradlew clean itest

> Task :source-sets:itest

com.baeldung.itest.SourceSetsItest > givenImmutableList_whenRun_ThenSuccess PASSED

So what happened? In this case, the guava dependency belongs to itestImplementation.

Unfortunately, the Eclipse Buildship Gradle plugin does not handle these custom configurations very well.

Let’s fix this in our build.gradle:

apply plugin: "eclipse"

// previous declarations

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

Let’s explain what we did here. We appended our configuration to the Eclipse classpath.

If we refresh the project, the compilation issues are gone.

However, there’s a drawback to this approach: The IDE does not distinguish between configurations.

This means we can easily import guava in our test sources (which we specifically wanted to avoid).

4. Conclusion

In this tutorial, we covered the basics of Gradle source sets.

Then we explained how custom source sets work and how to use them in Eclipse.

As usual, we can find the complete source code over on GitHub.


« 上一篇: 测试Quarkus应用程序
» 下一篇: ArrayStoreException异常