Java Module 14 – Testing, build tools and code quality
Gradle: Kotlin DSL, toolchains and tasks
Build a Java project with Gradle: write the Kotlin DSL build script, compile and test with a Java 25 toolchain, run tasks and commit the wrapper.
What you will learn
- Create a Java project with Gradle's Kotlin DSL
- Configure a Java toolchain so that Gradle compiles and tests with JDK 25 whatever JDK runs Gradle
- Run, test and package a project with Gradle tasks and the wrapper
- Convert a Maven build to Gradle and check that its tests give the same results
Before you start
What you’ll learn · Before you start
What you will learn
- Create a Java project with Gradle's Kotlin DSL
- Configure a Java toolchain so that Gradle compiles and tests with JDK 25 whatever JDK runs Gradle
- Run, test and package a project with Gradle tasks and the wrapper
- Convert a Maven build to Gradle and check that its tests give the same results
Before you start
On this page
Gradle is a build tool that models a build as a graph of tasks. A build script, build.gradle.kts in Gradle’s
Kotlin DSL, applies plugins that add tasks: the java plugin adds compileJava, test, jar and build, and
asking for build runs every task it depends on, in order. Gradle skips a task whose inputs and outputs have not
changed since its last run, which keeps repeated builds fast. A toolchain names the Java version that compiles and
tests the code, separately from the JDK that runs Gradle itself, and the wrapper, gradlew, fixes the Gradle
version for everyone who builds the project. Android apps are built with Gradle, and so are many large Java projects.
This lesson ports the Maven exercise pack, an invoice library with four JUnit tests, to Gradle 9.7.1. Gradle runs on Java 21 here, while the toolchain compiles and tests with Eclipse Temurin 25.0.4.1. Gradle 9.8.1 is the newest release; the wrapper section shows how a project moves to it.
Maven and Gradle, side by side
| Maven | Gradle |
|---|---|
pom.xml |
settings.gradle.kts and build.gradle.kts |
| Phases, with goals bound by the packaging | Tasks, added by plugins and linked by dependencies |
mvn package |
gradle build, or gradle jar for the JAR alone |
| Scopes compile, provided, runtime and test | Configurations implementation, compileOnly, runtimeOnly and testImplementation |
maven.compiler.release |
A Java toolchain, or the compiler’s release option |
~/.m2/repository |
~/.gradle/caches |
mvnw |
gradlew |
The scope row follows Gradle’s guide to migrating from Maven, which maps each scope to the configuration that replaces it.
The build script
invoice-tools/settings.gradle.kts
// The build's name, and the projects it contains: here only the root project.
rootProject.name = "invoice-tools" invoice-tools/build.gradle.kts
// The exercise pack built with Gradle's Kotlin DSL: the same sources and tests as the Maven version.
plugins {
// Adds the tasks that compile, test and package Java code (compileJava, test, jar, build ...).
java
}
group = "com.example"
version = "1.0.0"
java {
// Compile, test and run with a Java 25 toolchain, whatever JDK runs Gradle itself.
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
}
dependencies {
// The JUnit BOM fixes the versions of every JUnit artifact; the platform launcher runs the tests.
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
// Print each test's result in the build output.
testLogging {
events("passed", "skipped", "failed")
}
} invoice-tools/gradle.properties
# Where Gradle finds JDKs for toolchains: the folder named by the JDK_25 environment variable, and nowhere else, so
# the build uses the same JDK on every computer that sets it. Without these lines Gradle also searches the usual
# install locations.
org.gradle.java.installations.fromEnv=JDK_25
org.gradle.java.installations.auto-detect=false
org.gradle.java.installations.auto-download=false The settings file names the build and its projects. The build script applies the java plugin, sets the
coordinates, asks for a Java 25 toolchain and declares Maven Central as the repository. Its three test dependencies
are the JUnit BOM, which fixes the version of every JUnit artifact, JUnit Jupiter, and the platform launcher that
Gradle uses to start JUnit. The test block runs the tests on the JUnit Platform and prints each result. The
gradle.properties lines tell Gradle to take its JDKs from the JDK_25 environment variable only, so the build finds
the same JDK on every computer that sets it.
Tasks form a graph
The task graph of gradle build in a Java project
Text description of the diagram
The diagram shows the tasks that the java plugin adds and that gradle build runs, with an arrow from each task to a task that runs after it and needs its result.
- compileJava compiles src/main/java with the toolchain's compiler, and classes collects the compiled main classes.
- From classes, one branch builds the JAR: jar writes build/libs/invoice-tools-1.0.0.jar, and assemble is the lifecycle task for everything the project produces.
- The other branch tests: compileTestJava compiles src/test/java against the main classes, testClasses collects them, test runs JUnit, and check is the lifecycle task for every verification.
- build depends on both assemble and check, so it runs both branches.
The two resource tasks, processResources and processTestResources, also run before classes and testClasses. They are left out of the diagram because this project has no resources to copy, which the build log reports as NO-SOURCE.
from session import run
# Gradle runs on Java 21 (JAVA_HOME); the build's toolchain compiles and tests with Java 25. The first build of a
# project also compiles its Kotlin build script and downloads JUnit.
run(
"""
gradle --version | grep -E "^(Gradle|Launcher JVM|Daemon JVM)"
gradle build
ls build/libs build/test-results/test
""",
folder="invoice-tools",
show=("java", "gradle"),
java_home="21",
) Output
# openjdk version "25.0.4.1" LTS · Gradle 9.7.1 · JAVA_HOME: Java 21.0.12 $ gradle --version | grep -E "^(Gradle|Launcher JVM|Daemon JVM)" Gradle 9.7.1 Launcher JVM: 21.0.12.1 (Eclipse Adoptium 21.0.12.1+1-LTS) Daemon JVM: …/installs/java/temurin-21.0.12+101.0.LTS (no Daemon JVM specified, using current Java home) $ gradle build > Task :compileJava > Task :processResources NO-SOURCE > Task :classes > Task :jar > Task :assemble > Task :compileTestJava > Task :processTestResources NO-SOURCE > Task :testClasses > Task :test InvoiceTest > rejectsAZeroQuantity() PASSED InvoiceTest > totalsTheInvoice() PASSED InvoiceTest > roundsTaxHalfUpToTwoPlaces() PASSED InvoiceTest > addsTaxToEachLine() PASSED > Task :check > Task :build BUILD SUCCESSFUL in … 4 actionable tasks: 4 executed Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.7.1/userguide/configuration_cache_enabling.html $ ls build/libs build/test-results/test build/libs: invoice-tools-1.0.0.jar build/test-results/test: binary TEST-com.example.invoice.InvoiceTest.xml
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_session.py
Gradle itself ran on Java 21, as its launcher and daemon lines show, while the toolchain compiled and tested with
Java 25. The build passed through 11 tasks in the graph’s order. Four of them did real work, compileJava, jar,
compileTestJava and test, which is what “4 actionable tasks: 4 executed” counts; the others group tasks or had no
sources, marked NO-SOURCE. The 4 tests are the Maven version’s tests, and they passed again. The JAR landed in
build/libs and the test results in build/test-results/test, where Maven used target. The last line of the build
suggests the configuration cache, which reuses the result of evaluating the build scripts in later builds.
The order came from the graph, not from the build script: the script never mentions compileTestJava, and Gradle
still ran it after classes, because the test classes need the main classes to compile. Asking for a single task
runs only the part of the graph it needs, so gradle jar compiles the main code and stops without running a test,
and gradle tasks lists every task the plugins added.
Four ideas to know by name
- Kotlin DSL.
build.gradle.ktsis Kotlin code that the IDE checks as you type; the Groovy DSL,build.gradle, drives the same API. - Toolchain. The JDK that compiles, tests and runs the code, chosen by Java version from the JDKs Gradle knows.
- Wrapper.
gradlew,gradlew.batand thegradle/wrapperfolder, committed so that./gradlewruns the project’s Gradle version. - Up-to-date checks. A task that declares its inputs and outputs is skipped when neither has changed.
Once a project has the wrapper, every command in this lesson becomes ./gradlew followed by the same task names,
which is the form a build server should use. Android projects use the same wrapper, tasks and Kotlin DSL through the
Android Gradle plugin, so these four ideas carry over to Android builds.
Check yourself
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Toolchains in depth: one JDK runs Gradle, another compiles and tests Pro: opens with a Pro, Premium or Ultimate pass
Gradle runs on one JDK and builds with another, and confusing the two causes most builds that pass on a laptop and fail on a build server. This part shows how Gradle finds toolchains and what it does when it cannot.
In this part:
- Which JDK does what, measured on this build
- A toolchain Gradle cannot find, reproduced
- The compiler’s release option, with a single JDK
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Up-to-date checks measured: what Gradle skips, and a task that declares its inputs Pro: opens with a Pro, Premium or Ultimate pass
Gradle decides whether to run a task by comparing its inputs and outputs with those of its last run. This part measures that decision on the exercise pack, then writes a task that takes part in it.
In this part:
- The same build twice, then a change that leaves the bytecode alone
- A task of our own, with declared inputs and outputs
- What each task outcome means
2 runnable examples (Kotlin, Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
From Maven to Gradle: the converted pack, what init missed, and identical test results Pro: opens with a Pro, Premium or Ultimate pass
gradle init can convert a POM, and its output is a starting point rather than a finished build. This part converts
the exercise pack, fixes what the conversion missed and checks that the tests give the same results.
In this part:
- What gradle init wrote, and its first build
- Four corrections, then the same four tests
- The wrapper files, and moving to a new Gradle version
- Exercise: port the pack by hand
2 runnable examples (Kotlin, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
-
Fresher · Warm-up
What does the Gradle wrapper do, and which of its files do you commit?
The wrapper is a pair of scripts, gradlew for Linux and macOS and gradlew.bat for Windows, that runs the Gradle version the project names, downloading it first if this computer does not have it. The version and the download address are in gradle/wrapper/gradle-wrapper.properties, and gradle/wrapper/gradle-wrapper.jar holds the small program that does the download. Commit all four files, so that every developer and every build server runs ./gradlew build with the same Gradle, without installing Gradle by hand. To move to a newer version, run ./gradlew wrapper --gradle-version followed by the version, and commit the changed files; a SHA-256 checksum in the properties file makes the wrapper verify the download.
-
Fresher to mid
You add a Java 25 toolchain to a Gradle build. It works on every laptop, but CI fails with "Cannot find a Java installation on your machine matching languageVersion=25". The CI image has only Java 21. What do you do, and how do you keep the laptops and CI on the same JDK afterwards?
-
Mid to senior
A custom task that generates source code from a schema runs on every Gradle build, even when nothing changed, and adds about 40 seconds to each build. How do you find out why Gradle never skips it, and how do you fix it?
-
Mid to senior
Your team converted a Maven project to Gradle with gradle init. Before CI switches over, how do you show that the Gradle build produces the same thing as the Maven build, and what differences do you expect to find?
Key takeaways
- A Gradle build is a graph of tasks;
buildran 11 tasks, 4 of which did real work. - The toolchain decides which Java compiles and tests the code, whatever JDK runs Gradle itself.
- Maven scopes become configurations:
implementation,compileOnly,runtimeOnlyandtestImplementation. - Commit the wrapper so that everyone runs the same Gradle version with
./gradlew.
References
- Gradle User Manual (Gradle)
- Building Java Projects (Gradle)
- Toolchains for JVM projects (Gradle)
- Gradle Wrapper Basics (Gradle)
- The Java Plugin (Gradle)
- Testing in Java and JVM projects (Gradle)
- Incremental build (Gradle)
- Understanding Tasks: task outcomes (Gradle)
- Migrating Builds From Apache Maven (Gradle)
- Build Init Plugin: the pom build type (Gradle)
- Declaring repositories: local repositories (Gradle)
- Gradle Kotlin DSL Primer (Gradle)
- Upgrading your build from Gradle 9.x: deprecation of Kotlin DSL delegated properties (Gradle)
- Gradle Releases (Gradle)
- Configure your build (Android Gradle plugin) (Android Developers)
- Compatibility Matrix (Gradle)
- The Java Virtual Machine Specification, Java SE 25: 4.1 The ClassFile Structure (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress