left-icon

Groovy Succinctly®
by Duncan Dickinson

Previous
Chapter

of
A
A
A

CHAPTER 6

Larger Applications

Larger Applications


Creating scripts with Groovy is both easy and beneficial, but don't limit your use of this versatile language to just scripts! The best way to build larger applications with Groovy (and Java) is the Gradle build tool. Gradle will help you manage your project's dependencies, compilation, testing, and deployment. In this chapter, we'll walk through a small Gradle project and get a feel for how it works.

You’ll find the codebase described in this chapter in the gradle subdirectory of the sample code. We won't go through Gradle installation because the sample code for this chapter will sort that out for you. However, eventually you'll most likely want to install Gradle, so head over to the comprehensive documentation and you'll find the installation instructions.

Note: In the code listing for this chapter, you'll find some syntax not discussed in this e-book. I won't describe all of the syntax here, but it should be reasonably self-evident, and I'll recommend some resources for you to check out in the next chapter.

The build file

Take a look at Code Listing 82: The build.gradle File. What does it look like? That's right—Gradle uses Groovy to define the build file. This makes it quite readable (much nicer than XML) and also lets you use Groovy code to extend and customize your more complex builds.

Code Listing 82: The build.gradle File

apply plugin: 'groovy'
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.5'
    compile 'commons-cli:commons-cli:1.2'
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
}

mainClassName = 'demo.App'

Code Listing 82 performs the following sequence:

  1. Two plugins are applied:
  1. The groovy plugin provides the functionality required to build Groovy projects.
  2. The application plugin packages a distribution that makes it easy to share the application we're building.
  1. The repositories section tells Gradle to use Bintray's JCenter repository—as discussed in the section on Libraries.
  2. The dependencies section lists all of the project's dependencies. Each dependency descriptor uses the same syntax we've been using with Grape and is assigned to a configuration:
  1. Dependencies in the compile configuration are required to compile our code. This project is using Groovy 2.4.5 as well as the Apache Commons CLI library.
  2. Those in the testCompile configuration are required to run the test code in order. In this project, we'll use the Spock Framework for creating tests.
  1. The final item in the build file sets the mainClassApp property as the class with which the end user will run our application. This is used by the application plugin when preparing the distribution.

Take a look at the files for the project and you'll see the structure illustrated in Figure 3: Gradle Project Layout: 

  1. README.md is a markdown file used to provide a brief overview of the project.
  2. The build.settings file performs a role in builds more complex than build.gradle, but, for this project, it simply holds the project's name.
  3. The gradle directory contains files used by the Gradle Wrapper to download Gradle for use with the project. The Gradle Wrapper prepares two script files: gradlew for Unix systems and gradlew.bat for Windows systems.

The src directory contains the project's source code and test code. Using the standard configuration, Groovy files are located in src/main/groovy and the Spock tests are located in src/test/groovy. I've set up my source code into a demo package, hence you see src/main/groovy/demo. I've also got some resource files for my testing, so I’ve placed them into src/test/resources.

In order to build the project, just run ./gradlew build (Unix) or gradlew.bat build (Windows). This may take a little while on your first run because Gradle will fetch a variety of dependencies.

├── README.md

├── build.gradle

├── gradle

│   └── wrapper

│       ├── gradle-wrapper.jar

│       └── gradle-wrapper.properties

├── gradlew

├── gradlew.bat

├── settings.gradle

└── src

    ├── main

    │   └── groovy

    │       └── demo

    │           ├── App.groovy

    │           └── DataSummary.groovy

    └── test

        ├── groovy

        │   └── demo

        │       ├── AppTest.groovy

        │       └── DataSummaryTest.groovy

        └── resources

            ├── sample-data-1.txt

            ├── sample-data-2.txt

            ├── sample-data-3.txt

            ├── sample-data-4.txt

            └── sample-data-5.txt

Figure 3: Gradle Project Layout

Once the build has completed, you'll find that a new build subdirectory has been created. Take a look in this directory and you'll see a reports subdirectory containing an index.html file. Open this file in your favorite browser and you'll see the results of the Spock tests—I trust that they all passed. You'll also find a distributions subdirectory—this contains two archive files (zip and tar). These archives contain the distribution items needed to run our application, including start scripts (in the bin directory) and the required libraries (in the lib directory).

Extract one of the archives into a directory and open a new terminal/command prompt at the base of this directory. Run bin/gradle-demo –h (Unix) or bin\gradle-demo.bat –h and you'll see the usage information for the application.

The sample application provided for this book will read in a file containing a list of numbers (one number per line) and will display a basic summary—for an example, see Figure 4: Sample Application Output. In order to read in a file, we use the –f flag followed by a file path. Copy the test files found in src/test/resources to the directory into which you extracted the distribution. You can then run the application with bin/gradle-demo -f sample-data-1.txt (Unix) or bin/gradle-demo -f sample-data-1.txt (Windows).

Count: 6
Max: 9
Min: 0
Sum: 23
Average: 3.8333333333

Figure 4: Sample Application Output

The code

The codebase for the project consists of two classes, DataSummary and App. Both of these are in the demo package.

Tip: If you'd like to "decode" some of the code provided here, check out the Groovy documentation for object-oriented programming.

Code Listing 83: The DataSummary Class is reasonably simple, and the constructor does the heavy lifting of determining the summary for the provided list of numbers. I'll draw your attention to one line of code, data = sourceData.asImmutable(), as it demonstrates the very handy asImmutable() method of creating a copy of the sourceData parameter and prevents the contents of the copied list from being changed. It's such as useful tool for defensive coding that I'd feel bad if I didn’t point it out!

Code Listing 83: The DataSummary Class

package demo

class DataSummary {
    final List data

    final Integer count
    final BigDecimal sum, average, max, min

    def DataSummary(List sourceData) {
        if (!sourceData)
            throw new IllegalArgumentException
                    ('The source list cannot be empty')

        data = sourceData.asImmutable()

        count = data.size()
        max = data.max()
        min = data.min()
        sum = data.sum()
        average = sum / count
    }

    String toString() {
        """\
Count: $count
Max: $max
Min: $min
Sum: $sum
Average: $average"""
    }
}

Code Listing 85: The App Class provides the primary application entry point. The main class method is the standard entry point for Groovy (and Java) applications. In the case of the Groovy scripts in the previous chapter, the main method is created for us, but we need to create an entry point for classes. The App class provides a main method, and our Gradle configuration indicates that this is the entry point for our application through the mainClassName = 'demo.App' setting in the build.gradle file.

The App class's main method has a single parameter called args that will hold a list of all of the command-line arguments passed to the application. The choice of this parameter name stems from common Java usage and can be found in Groovy scripts as well. None of the scripts we’ve seen previously have used the command line, so I'll provide a basic demonstration in Code Listing 84: A Basic Script that Lists Args. If you were to call this script with something like groovy cli.groovy -f test.txt, you'd see the two command-line arguments (-f and test.txt) displayed each on a separate line. Essentially, the args variable is a built-in script variable.

Code Listing 84: A Basic Script that Lists Args

args.each {
    println it
}

We could prepare code to parse the command-line arguments, but Groovy provides us with the CliBuilder class, which is built on the Apache Commons CLI library. In order to set up a CliBuilder object, we pass it a string that provides the usage statement for the application—for the App class I've used new CliBuilder(usage: 'App [options]'). Next, let’s use a static initializer block (static {..}) to prepare the cli class property. Within the initializer block, I've used that handy with method and prepared the list of acceptable command-line arguments. For each entry, we can provide the short form of the option (e.g., f for –f) and a number of additional items:

  • The long form (longOpt) for more literate flags (e.g., file for --file).
  • The number of expected arguments for the option. For the –f flag we expect one argument, the name of the input file.
  • A display name for the option's argument (e.g., inputFile).
  • A user-friendly description of option.

This sets up the command-line interface for our application. By calling cli.usage(), we can have our usage displayed in a common format, as demonstrated in Figure 5: Usage Display. 

usage: App [options]

 -f,--file <inputFile>   The input file with one number per line

 -h,--help               Displays the usage information

Figure 5: Usage Display

Within the main method, we can have the command-line arguments parsed by simply calling def options = cli.parse args. When this finishes, we can access the arguments using options.h and options.f. You'll see in Code Listing 85: The App Class that I check if the application was passed the –h flag and, if so, displays the usage information. Otherwise, we expect that the application was called with –f and a file path. At various points marking the end of the application, System.exit is called with a number to indicate the exit status—usually 0 if there were no problems, or a number if there was a problem. I also use System.err.println to output error messages to standard error.

Tip: While Commons CLI library is available by default to Groovy scripts, you'll recall that I've added it as an explicit dependency for the Gradle project. This is required because the groovy-all library doesn't include commons-cli.

There are a few other syntax aspects in Code Listing 85: The App Class that I haven't previously covered. I've used the basic form of the if statement to check the command-line arguments. This consists of the if keyword, a conditional expression within parentheses—(options.h)—and a statement that is evaluated if the conditional expression resolves as true. Groovy evaluates options.h to be false if there is no value, so we don't have to check it against something. In fact, values such as 0 (zero), an empty string (''), and null are all seen as false to Groovy, which reduces the syntax needed in this type of check.

The last syntax element you'll notice is the throwing of exceptions—throw new FileNotFoundException("Path does not exist: $fileName")—and exception handling with the try-catch block. However, I'll leave you to pursue other resources to find out more about this.

Code Listing 85: The App Class

package demo

import java.nio.file.Files
import java.nio.file.Paths

class App {
    static final cli =
            new CliBuilder(usage: 'App [options]')

    static {
        cli.with {
            f longOpt: 'file',
                    args: 1,
                    argName: 'inputFile',
                    'The input file with one number per line'
            h longOpt: 'help',
                    'Displays the usage information'
        }
    }

    static loadNumberListFromFile(String fileName) {
        def path = Paths.get(fileName)

        if (!Files.isRegularFile(path))
            throw new FileNotFoundException
                    ("Path does not exist: $fileName")

        path.readLines().findAll {
            it.isNumber()
        }.collect {
            it.toBigDecimal()
        }
    }

    static void main(args) {

        def displayHelpAndExit = {
            cli.usage()
            System.exit 0
        }

        def displayErrorAndExit = { message, errorNumber = -1 ->
            System.err.println "Error: $message"
            System.exit errorNumber
        }

        def options = cli.parse args

        if (options.h)
            displayHelpAndExit()

        if (!options.f)
            displayErrorAndExit 'No input file provided', -2

        def inputData = null
        try {
            inputData = loadNumberListFromFile options.f
        } catch (any) {
            displayErrorAndExit "${any.message}", -3
        }

        if (!inputData)
            displayErrorAndExit "No numeric data in ${options.f}", -4

        println "${new DataSummary(inputData)}"

        System.exit 0
    }
}

Spock tests

The Spock Framework provides facilities for writing unit, behavior-driven, integration, and functional tests for Java and Groovy code. That's right, even if you have to write Java for your application code, you can use Spock to test it. And what are Spock tests written in? Yep, Groovy.

To get started with Spock in our Gradle project, we must declare the dependency for the testCompile configuration: testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'. This means that the Spock library will be used to run tests, but it will not be included in the distribution. In order to set up our tests, we need only to add the test code to the src/test/groovy directory. The tests will be performed when you run gradlew build, but you can specifically run the tests by calling gradlew test.

Spock leverages Groovy to provide an easily readable approach to writing tests. To see how far this goes, check out Code Listing 86: Spock Test for the DataSummary Class. In order to declare the test class, we extend Spock's Specification class, then provide one or more tests, each defined as a method. There's a high level of human readability in the code:

  • The test (method) name is a sentence that describes the test. That's right, def "Trivial DataSummary Test with a basic data table"() is an acceptable Groovy method signature.
  • The test is divided into blocks, each containing an aspect of the test:
  • The given block provides initialization items for the test.
  • The when block provides the action for the test and is followed by a then block to check the result.
  • The expect block contains conditions that the test must meet in order to pass and represents a conflation of the when and then blocks.
  • The where block provides input and output data for the test.

Each block can be accompanied with a piece of text to explain what's going on. You'll see that the tests provided here use a mix of blocks, depending on what action needs to be performed.

Code Listing 86: Spock Test for the DataSummary Class

package demo

import spock.lang.Specification
import spock.lang.Unroll

class DataSummaryTest extends Specification {
    @Unroll("Number list: #numbers")
    def "Trivial DataSummary Test with a basic data table"() {
        given: "A new DataSummary object"
        def summary = new DataSummary(numbers)

        expect: "That the stats are correct"
        summary.count == count
        summary.max == max
        summary.min == min
        summary.sum == sum
        summary.average == average

        where: "The input data and expected summaries are"
        numbers        || count | min | max | sum | average
        [ 10 ]         || 1     | 10  | 10  | 10  | 10
        [ 10, 12 ]     || 2     | 10  | 12  | 22  | 11
        [ 10, 12, 14 ] || 3     | 10  | 14  | 36  | 12
    }

    def "Ensure that an IllegalArgumentException is thrown with an empty list"() {
        when: "I try to create a new DataSummary with an empty list"
        new DataSummary([ ])
        then: "the correct exception should be thrown"
        thrown(IllegalArgumentException)
    }
}

To my mind, Spock's approach to data-driven testing is a key selling point for the framework. In both of the test classes, I've prepared a data table in the where block that allows me to easily provide a series of test inputs alongside the desired result. The format of the data table is elementary:

  • The first row is the header and provides the data variable names. These names can be used within the test code and are replaced by the associated data value from the subsequent row(s).
  • Subsequent rows provide input and output data. Input data is provided to the left of the double pipe (||) and output data to the right. Spock doesn't enforce this formatting, but I use it to aid readability. A single pipe delineates values within the input/output section.

Each (nonheader) row represents an iteration of the test. Therefore, a table with three (nonheader) rows will cause the test method to be run three times. Take a look at the data table in Code Listing 86: Spock Test for the DataSummary Class and you'll notice that we can even declare lists (and other objects) within the data table. The data variables are then accessed as variables within the test.

The @Unroll annotation is used to report each test iteration independently. This is useful in determining a problem, especially because we can use data variable names in the test's descriptor. For example, @Unroll("Number list: #numbers") results in a single test iteration being named Number list: [10, 12, 14] in the test report. By prefixing the hash/pound symbol (#) to the data variable name, we've advised Spock to perform a substitution. In fact, we don't need to provide this within the @Unroll annotation and could have used the data variable names in the test's method declaration (e.g., def "Trivial DataSummary Test with number list: #numbers"()).

In both of the example test classes, I've also demonstrated our ability to check that an exception was thrown when running the test. For example, in Code Listing 87: Spock Test for the App Class, I make sure that the call to App.loadNumberListFromFile('') causes an exception by checking for thrown(FileNotFoundException).

Code Listing 87: Spock Test for the App Class

package demo

import spock.lang.Specification
import spock.lang.Unroll

class AppTest extends Specification {
    @Unroll("Input file: #file")
    def "Testing loadNumberListFromFile"() {
        expect:
        App.loadNumberListFromFile("src/test/resources/$file") == result

        where: "The input file has an associated result"
        file                || result
        'sample-data-1.txt' || [ 5, 6, 2, 9, 0, 1 ]
        'sample-data-2.txt' || [ 5, 6, 2, 9, 0, 1 ]
        'sample-data-3.txt' || [ ]
        'sample-data-4.txt' || [ 5, 6, 2, 9, 0, 1 ]
        'sample-data-5.txt' || [ -5, 6, 2, -9.3, 0.2, 1 ]
    }

    def "Ensure that a FileNotFoundException is thrown for absent files"() {
        when: "I try to load a file that doesn't exist"
        App.loadNumberListFromFile('')
        then: "the correct exception should be thrown"
        thrown(FileNotFoundException)
    }
}

Summary

Gradle is a great step forward for build systems. It's more readable than XML-based approaches and easily customized and extended with Groovy. Investing some time in exploring Gradle and the range of community-submitted plugins will be worthwhile.

This chapter also introduced you to the Spock Framework. Spock provides an extremely versatile approach to testing that blends technical and narrative aspects. I’ve only scratched the surface here of what's possible with Spock, and I encourage you to look into it further.

Both Gradle and Spock employ the strengths of Groovy and can be utilized whether or not you use Groovy in your application code.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.