Getting Started with TDD in Gradle for Java
Learn to set up a Java project through lots of errors
Yesterday, I began creating a mini Java server that can respond to HTTP requests. Realizing the scope of the project and my relative unfamiliarity with both HTTP and Java, I knew that I’d have to start strong with tests. I spent a day practicing Java on exercism.io, and I realized that they used Gradle to compile and run their code and tests.
What is Gradle?
The first time I used Gradle on exercism, I thought that Gradle was a testing framework. Prior to architecting my project, my only exposure to Gradle had been the gradle test
command. Looking up “what is gradle” suggested that I was wrong, but I still didn’t know what Gradle was. According to Wikipedia:
Gradle is an open-source build automation system that builds upon the concepts of Apache Ant and Apache Maven and introduces a Groovy-based domain-specific language instead of the XML form used by Apache Maven for declaring the project configuration. Gradle uses a directed acyclic graph to determine the order in which tasks can be run.
A majority of those words made very little sense to me, so I decided to build something with it and learn along the way with a HelloGradle project.
Installing Gradle
If you don’t already have Gradle installed, you can install it using a package manager. I use Homebrew, but the Gradle website includes instructions for SDKMAN!, Homebrew, Scoop, Chocolatey, and MacPorts. If you prefer, you can also download and install Gradle manually.
First, you’ll need to create a directory. We’ll call it HelloGradle.
mkdir HelloGradle
Our directory is empty now, but once we initialize a Gradle build, we’ll see it populate a few new directories and files.
gradle init
Your previously empty directory should now look like this:
.gradle/
.settings/
gradle/
.project
build.gradle
gradlew
gradlew.bet
settings.gradle
The most important file above will be build.gradle. We will enter our “Groovy-based domain-specific language” configurations inside of this file. Because Gradle can build in a number of different languages, we should configure our build for Java. According to Gradle:
The Java plugin adds Java compilation along with testing and bundling capabilities to a project. It serves as the basis for many of the other Gradle plugins.
We did say that we want testing, so we’re definitely heading in the right direction. To add the Java plugin, simply add the following line to your build.gradle
apply plugin: 'java'
Gradle is based on tasks made to automate aspects of your project. You can define tasks yourself, or you can rely on the basic tasks that Gradle provides us. I won’t go into defining tasks , but you can learn more about that here. We can quickly look at the tasks available to us by running gradle-tasks
.
~/HelloGradle$ gradle tasks> Task :tasks------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'HelloGradle'.
components - Displays the components produced by root project 'HelloGradle'. [incubating]
dependencies - Displays all dependencies declared in root project 'HelloGradle'.
dependencyInsight - Displays the insight into a specific dependency in root project 'HelloGradle'.
dependentComponents - Displays the dependent components of components in root project 'HelloGradle'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'HelloGradle'. [incubating]
projects - Displays the sub-projects of root project 'HelloGradle'.
properties - Displays the properties of root project 'HelloGradle'.
tasks - Displays the tasks runnable from root project 'HelloGradle'.Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.To see all tasks and more detail, run gradle tasks --allTo see more detail about a task, run gradle help --task <task>BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
These tasks aren’t super helpful for us since we don’t have any Java code written in our project yet. Before we create any new files, though, we should take a look at the architecture that Gradle expects our project to follow.
Project Architecture
Since we’re test-driven developers, we’ll begin by creating the Test Java source directory and filling it with a test.
mkdir -p src/test/java
Our test won’t be too complicated. It’ll simply check that the sayHello
method of the HelloGradle
class returns “Hello, World!”. We’ll use the junit library to write our tests.
As previously noted, it’s super simple to run tests with the gradle test
command. Unfortunately, it won’t work quite yet.
~/HelloGradle$ gradle test> Task :compileTestJava FAILED
/Users/omard/HelloGradle/src/test/java/HelloGradleTest.java:1: error: package org.junit does not exist
import org.junit.Test;
^
/Users/omard/HelloGradle/src/test/java/HelloGradleTest.java:2: error: package org.junit does not exist
import static org.junit.Assert.assertEquals;
^
/Users/omard/HelloGradle/src/test/java/HelloGradleTest.java:2: error: static import only from classes and interfaces
import static org.junit.Assert.assertEquals;
^
/Users/omard/HelloGradle/src/test/java/HelloGradleTest.java:6: error: cannot find symbol
@Test
^
symbol: class Test
location: class HelloGradleTest
/Users/omard/HelloGradle/src/test/java/HelloGradleTest.java:8: error: cannot find symbol
assertEquals("Hello, world!", new HelloGradle().sayHello());
^
symbol: class HelloGradle
location: class HelloGradleTest
5 errorsFAILURE: Build failed with an exception.* What went wrong:
Execution failed for task ':compileTestJava'.
> Compilation failed; see the compiler error output for details.* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.* Get more help at https://help.gradle.orgBUILD FAILED in 0s
1 actionable task: 1 executed
The first three errors (in bold above) seem to be unhappy with importing org.junit.Test
and org.junit.Assert.assertEquals
. That’s because Gradle wants to handle our dependencies. To handle our test dependency, we’ll add the line below. As of the writing of this article, 4.12 is the most recent, stable version of JUnit.
If we run gradle test
once more, we’ll see a new error.
~/HelloGradle$ gradle test
> Task :compileTestJava FAILEDFAILURE: Build failed with an exception.* What went wrong:
Could not resolve all files for configuration ':testCompileClasspath'.
> Cannot resolve external dependency junit:junit:4.12 because no repositories are defined.
Required by:
project :* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.* Get more help at https://help.gradle.orgBUILD FAILED in 0s
1 actionable task: 1 executed
In order to use external packages, we’ll need to choose a repo from which to pull our dependencies. In this case, we’ll use Maven Central.
After every one of those errors, we finally return to a useful error.
~/HelloGradle$ gradle test> Task :compileTestJava FAILED
/Users/omard/HelloGradle/src/test/java/HelloGradleTest.java:8: error: cannot find symbol
assertEquals("Hello, world!", new HelloGradle().sayHello());
^
symbol: class HelloGradle
location: class HelloGradleTest1 errorFAILURE: Build failed with an exception.* What went wrong:
Execution failed for task ':compileTestJava'.
> Compilation failed; see the compiler error output for details.* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.* Get more help at https://help.gradle.orgBUILD FAILED in 0s
1 actionable task: 1 executed
We should have been expecting this error since we haven’t built our HelloGradle class!
mkdir -p src/main/java
The fourth time we run gradle test
, we finally get a successful build.
~/HelloGradle$ gradle testBUILD SUCCESSFUL in 0s
3 actionable tasks: 3 up-to-date
If you also felt a little underwhelmed and under-informed by the message above, there’s a fix. You can configure your tests so that they log to the console. In our case, we want to log passed, failed, and skipped tests (rather than just the failed tests, which are logged by default).
~/HelloGradle$ gradle test> Task :testHelloGradleTest > testThatGradleIsGreeted PASSED
For even more configuration options, check out the documentation for the TestLoggingContainer.
Creating a Jar File
Now let’s say that you want to show your greeter off to the world. To do so, you’ll want to run a .jar file. Conveniently, gradle creates one for us. First, you should navigate to the location of that file and run the .jar file. Unfortunately, that will produce an error (in bold below).
~/HelloGradle$ cd -P build/libs/
~/HelloGradle/build/libs$ java -jar HelloGradle.jar
no main manifest attribute, in HelloGradle.jar
We can solve this issue with an additional configuration in our build.gradle.
The jar
configuration builds our jar file after we provide the task with the name of the main class in our project. From here, we can navigate to the root folder and re-assemble our build.
~/HelloGradle/build/libs$ cd ../..
~/HelloGradle/build/libs$ gradle assembleBUILD SUCCESSFUL in 0s
2 actionable tasks: 1 executed, 1 up-to-date
Everything looks good from here, so we can run our .jar file once more.
~/HelloGradle/build/libs$ java -jar HelloGradle.jar
Error: Main method not found in class HelloGradle, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
This is as unambiguous as our errors are going to get. Since our test checks that the sayHello
method returns a value, we can simply have the main
method print to the console.
I’ll leave it up to you to reassemble the build. After you do, re-running the .jar file should give you:
~/HelloGradle/build/libs$ java -jar HelloGradle.jar
I can say hello from the console!
Now that your main class and tests are set up, you can begin building, testing, and sharing your Java projects!