Because the standard project layout of a Java project defines only one test directory (src/test), we have no standard way to add integration tests to our Gradle build.
If we want to use the standard project layout, we can add integration tests to our Gradle build by using one of the following options:
- We can add our integration tests to the same directory than our unit tests. This is an awful idea because integration tests are typically a lot slower than unit tests. If we decide to use this approach, the length of our feedback loop is a lot longer than it should be.
- We can create a new project and add our integration tests to that project. This makes no sense because it forces us to transform our project into a multi-project build. Also, if our project is already a multi-project build, we are screwed. We can of course add all integration tests to the same project or create new integration test project for each tested project, but it would be less painful to shoot ourselves in the foot.
It is clear that we need a better way. This blog post describes how we create a Gradle build that fulfils the following requirements:
- Integration and unit tests must have different source directories. The src/integration-test/java directory must contain the source code of our integration tests and the src/test/java directory must contain the source code of our unit tests.
- Integration and unit tests must have separate resource directories. The src/integration-test/resources directory must contain the resources of our integration tests. The src/test/resources directory must contain the resources of our unit tests.
- We must be able to configure compile time and runtime dependencies for our integration tests.
- We must be able to run either our unit tests or integration tests.
- We must be able to run all tests.
- If an integration test fails, our build must fail as well.
- Integration and unit tests must have separate HTML reports.
Let’s start by configuring the source and resource directories of our integration tests.
This example application of this blog post is tested with Gradle 4.6.
Additional Reading:
If you are not familiar with Gradle, you should read the following blog posts before you continue reading this blog post:
- Getting Started With Gradle: Introduction helps you to install Gradle, describes the basic concepts of a Gradle build, and describes how you can add functionality to your build by using Gradle plugins.
- Getting Started With Gradle: Our First Java Build describes how you can create a Java project by using Gradle and package your application to an executable jar file.
- Getting Started With Gradle: Dependency Management describes how you can manage the dependencies of your Gradle project.
- Getting Started With Gradle: Creating a Binary Distribution describes how you can create a runnable binary distribution that doesn’t use the so called “jar jar” approach.
Configuring the Source and Resource Directories of Our Integration Tests
We can add new source and resource directories to our Gradle build by using the sourceSets build script block. Armed with this information, we can configure the source and resource directories of our integration tests by following these steps:
- Create a new source set called integrationTest.
- Ensure that the output of the main and test source sets is added to the compile time classpath.
- Ensure that the output of the main and test source sets is added to the runtime classpath.
- Set the source directory of our integration tests to src/integration-test/java.
- Set the resource directory of our integration tests to src/integration-test/resources.
When we are done, our build.gradle file should have the following sourceSets build script block right after the repositories build script block:
1sourceSets {
2 integrationTest {
3 java {
4 compileClasspath += main.output + test.output
5 runtimeClasspath += main.output + test.output
6 srcDir file('src/integration-test/java')
7 }
8 resources.srcDir file('src/integration-test/resources')
9 }
10}
Sometimes you might not want that integration tests depend from unit tests. For example, you might want to use two different implementations of the same API. If this is the case, you should take a look at Jukka’s comment. Also, I recommend that you take a look at Jonathan’s comment that describes the difference between files() and file() methods.
Additional Reading:
When we run the command: gradle properties at the command prompt, we will see a long list of the project’s properties. The properties that are relevant for this blog posts are shown in the following:
1> gradle properties
2:properties
3
4------------------------------------------------------------
5Root project
6------------------------------------------------------------
7configurations: [configuration ':archives', configuration ':compile', configuration ':default', configuration ':integrationTestCompile', configuration ':integrationTestRuntime', configuration ':runtime', configuration ':testCompile', configuration ':testRuntime']
8
9sourceSets: [source set 'integration test', source set 'main', source set 'test']
10sources: [Java source 'main:java', JVM resources 'main:resources', Java source 'test:java', JVM resources 'test:resources', Java source 'integrationTest:java', JVM resources 'integrationTest:resources']
11
12BUILD SUCCESSFUL
13
14Total time: 3.34 secs
As we can see, we added a new source and resource directories to our Gradle build. The interesting this is that when we created a new source set, the Java plugin added two new dependency configurations to our build:
- The integrationTestCompile configuration is used to declare the dependencies that are required when our integration tests are compiled.
- The integrationTestRuntime configuration is used to declare the dependencies that are required to run our integration tests. This configuration contains all dependencies that are added to the integrationTestCompile configuration.
Additional Reading:
Let’s move and find out what kind of configuration changes we have to make before these dependency configurations are useful to us.
Configuring the Dependency Configurations of Our Integration Tests
When we configured the source and resource directories of our integration tests, we created a source set that created two new dependency configurations: integrationTestCompile and integrationTestRuntime. The problem is that these configurations do not contain the dependencies of our unit tests.
We could solve this problem by adding the required dependencies to these configurations, but we won’t do that because adding duplicate configuration is an awful idea. Instead we will configure these dependency configurations by following these steps:
- Ensure that the integrationTestCompile configuration contains the dependencies that are required to compile our unit tests.
- Ensure that the integrationTestRuntime configuration contains the dependencies that are required to run our unit tests.
We can make these changes by using the configurations build script block. In other words, we must add the following code to our build.gradle file between the sourceSets and the dependencies build script blocks:
1configurations {
2 integrationTestCompile.extendsFrom testCompile
3 integrationTestRuntime.extendsFrom testRuntime
4}
Additional Reading:
We can now add dependencies to these configurations. For example, if we want to use AssertJ 3.0 in our integration tests, we have to add the assertj-core dependency to the integrationTestCompile configuration. After we have done this, the dependencies build script block found from our build.gradle file looks as follows:
1dependencies {
2 compile 'log4j:log4j:1.2.17'
3 testCompile 'junit:junit:4.11'
4 integrationTestCompile 'org.assertj:assertj-core:3.0.0'
5}
Additional Reading:
Our next step is to create the task that runs our integration tests. Let’s find out how we can do that.
Creating the Task That Runs Our Integration Tests
We can create the task that runs our integration tests by following these steps:
- Create a new task called integrationTest and set its type to Test.
- Configure the location of the compiled test classes.
- Configure the classpath that is used when our integration tests are run.
We can create and configure the integrationTest task by adding the following code to our build.gradle file:
1task integrationTest(type: Test) {
2 testClassesDirs = sourceSets.integrationTest.output.classesDirs
3 classpath = sourceSets.integrationTest.runtimeClasspath
4}
David pointed out that Gradle skips tasks whose input and output are up to date. If you want to ensure that your integration tests are run every time when you run the integrationTest task, you have to tell Gradle that the outputs of the integrationTest task should always be considered out of date.
A Gradle task has a property called outputs, and the type of this property is TaskOutputs. If you want that the outputs of the integrationTest task are always considered out of date, you have to ensure that the upToDateWhen() method of the TaskOutputs interface always returns false. After you have done this, the declaration of the integrationTest task looks as follows:
1task integrationTest(type: Test) {
2 testClassesDirs = sourceSets.integrationTest.output.classesDirs
3 classpath = sourceSets.integrationTest.runtimeClasspath
4 outputs.upToDateWhen { false }
5}
Additional Reading:
We have created the task that runs our integration tests, but the problem is this task is not invoked during our build. Because want to include it in our build, we have to follow these steps:
- Ensure that our integration tests are run before the check task and that the check task fails the build if there are failing integration tests.
- Ensure that our unit tests are run before our integration tests. This guarantees that our unit tests are run even if our integration tests fails.
We can do these configuration changes by adding the following lines to our build.gradle file:
1check.dependsOn integrationTest
2integrationTest.mustRunAfter test
Additional Reading:
We are done! Let’s move on and find out how we can run our tests.
Running Our Tests
We have now created a new task that runs our integration tests and integrated that task with our Gradle build. We are finally ready to run our unit and integration tests. The requirements of our Gradle build states that:
- We must be able to run our only unit tests.
- We must be able to run only integration tests.
- We must be able to run all tests.
Let’s go through these requirements one by one.
First, if we want to run only unit tests, we can use one of these two options:
- We can run our unit tests by running the command: gradle clean test at the command prompt.
- We can run our build and exclude integration tests by running the command: gradle clean build -x integrationTest at the command prompt.
Second, if we want to run only integration tests, we can choose one of the following options:
- We can run our integration tests by running the command: gradle clean integrationTest at the command prompt.
- We can run our build and exclude unit tests by running the command: gradle clean build -x test at the command prompt.
Third, if we want to run all tests, we can use one of these two options:
- We run our unit and integration tests by running the command: gradle clean test integrationTest at the command prompt.
- We can run our build by running the command: gradle clean build at the command prompt.
Additional Reading:
When we run our tests, Gradle creates the HTML reports of our unit and integration tests to the following directories:
- The build/reports/tests/integrationTest directory contains the HTML report that contains the test results of our integration tests.
- The build/reports/tests/test directory contains the HTML report that contains the test results of our unit tests.
Let’s summarize what we learned from this blog post.
Summary
This blog post has taught us the following things:
- If we add a new source set to our build, the Java plugin creates the compile time and runtime dependency configurations for it.
- We can include the dependencies of an another dependency configuration by using the extendsFrom property of the Configuration.
- We can create a task that run our integration tests by creating a new Test task, and configuring the location of the integration test classes and the used classpath.
- We can add dependencies to a task and configure the order in which our tasks are invoked.
- We can exclude tasks by using the -x command-line option.
P.S. You can get the example application of this blog post from Github.
P.P.S. You should take a look at the Gradle TestSets plugin. It allows you to specify additional test sets and add these test sets to your build.