Saturday, October 10, 2015

Getting started with Gradle

From wikipeia : 
Gradle is a build automation tool that builds upon the concepts of Apache Ant and Apache Maven and introduces a Groovy-based domain-specific language (DSL) instead of the more traditional XML form of declaring the project configuration. Gradle uses a directed acyclic graph ("DAG") to determine the order in which tasks can be run.
Gradle was designed for multi-project builds which can grow to be quite large, and supports incremental builds by intelligently determining which parts of the build tree are up-to-date, so that any task dependent upon those parts will not need to be re-executed.
The initial plugins are primarily focused around JavaGroovy and Scala development and deployment, but more languages and project workflows are on the roadmap.

1. Roadmap. 

Let's create imitation of complex application with several projects and several source directories: 
- we will have 3 different  projects with dependencies one from each others
- we will have different source directories (not only main/java)
- we will have different languages (java and groovy) in our application
- we will have source directory with integration tests, which we will be able to run separately from building application

2. Application structure.

For imitation of complex application I created 3 sub-projects : COMMON(empty now), API(I created just one interface here), APP(application itself - implementation of interface declared in API sub-project). 
inside APP sub-project, also for imitation of complex application, inside SRC directory, beside standard source directories(src/main and src/test) I created in addition directories : src/it(integration testing) and src/run(just main class to run application).
That is because I don't want integration tests to be running with unit tests so UNIT and INTEGRATION tests are in different directories : unit tests in standard "test", integration tests - in "it"  directory.
Also I decided to split code by language, so test in java are in directory "test/java", tests in groovy in "test/groovy". 
Full application structure : 



Structure of app sub-project:



3. main project files 

If we have different projects - we need settings.gradle file with just 1 line :

include ":api", ":common", ":app"
 
 
to inform gradle to look also into listed directories.

Also, of course, we need a main project file - build.gradle :

allprojects {
  task hello << { task -> println "I'm $task.project.name" }
}

subprojects {
  apply plugin: "java"  apply plugin: "groovy"
  repositories {
        jcenter()
   }

}

project(':common') { 
  dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.1'    compile "org.spockframework:spock-core:1.0-groovy-2.4"    compile "junit:junit:4.10"  }
} 

project(':api') { 
   dependencies {
      compile project(':common')
   }
} 

project(':app') {

    apply plugin:'application'    mainClassName = "App"
   dependencies {
      compile project(':api')
   }
   
    sourceSets {

        main {
            java {
                srcDirs=['src/main/java', 'src/run/java']
            }
        }

        integTest {
            groovy {
                srcDirs=['src/it/groovy']
            }
            compileClasspath = sourceSets.main.output + configurations.testRuntime
            runtimeClasspath = output + sourceSets.main.output + configurations.testRuntime
        }
    }
    
    task integTest(type: Test, dependsOn:test) {
        testClassesDir = sourceSets.integTest.output.classesDir
        classpath = sourceSets.integTest.runtimeClasspath
    }


}
Here I defined dependencies : "common" - main project, "api" - depend on common,  "app" - depend on api. All external dependencies (junit, spock ....) are defined in "common" sub-project and will be available for all dependend projects ("api" and "app").
  After applying plugins "java" and "groovy" gradle will automatically search for sources in directories "main/java", "main/groovy", "test/java", "test/groovy". To add also "main/run" - I had to "extend" current value of main source set : to add additional directory to it :
main {
    java {
        srcDirs=['src/main/java', 'src/run/java']
    }
}
 
Integration test - it's a independent source directory, so I have to define it : 
integTest {
    groovy {
        srcDirs=['src/it/groovy']
    }
    compileClasspath = sourceSets.main.output + configurations.testRuntime
    runtimeClasspath = output + sourceSets.main.output + configurations.testRuntime
} 

4. Api project - test interface

As I mentioned before, in api project I placed just one interface : 

package com.demien.gradletest;

public interface TestProcessor {
    String getGreetingWord();
}

5. App project - interface implementations

In App project I created 2 different implementations of test interface :

package com.demien.gradletest;
public class TestHelloProcessor implements TestProcessor {
    @Override    public String getGreetingWord() {
        return "Hello";
    }
}


package com.demien.gradletest;
public class TestHiProcessor implements TestProcessor {

    @Override    public String getGreetingWord() {
        return "Hi";
    }
}

6. App project - test class

Purpose of test class - just to be used in test. So it use implementation of interface to create greeting string : 
package com.demien.gradletest;

public class TestClass {

    TestProcessor testProcessor;

    public void setTestProcessor(TestProcessor testProcessor) {
        this.testProcessor = testProcessor;
    }

    public String greeting(String name) {
        return testProcessor.getGreetingWord() +", "+name;
    }
}

7. App project - run file

Of cource it's pointless and stupid - to create different source directory for app run class. 
It was made just learning work with source directories in Gradle :) 
So app/run/java/app.java class : 
import com.demien.gradletest.TestClass;
import com.demien.gradletest.TestHelloProcessor;
import com.demien.gradletest.TestHiProcessor;
import com.demien.gradletest.TestProcessor;


public class App {
    public static void main(String[] args) {
        TestProcessor testHelloProcessor=new TestHelloProcessor();
        TestProcessor testHiProcessor=new TestHiProcessor();
        TestClass testClass=new TestClass();

        testClass.setTestProcessor(testHelloProcessor);
        System.out.println(testClass.greeting("Joe"));


        testClass.setTestProcessor(testHiProcessor);
        System.out.println(testClass.greeting("Anna"));
    }
}

Result of execution : 
Hello, Joe
Hi, Anna

8. App - unit tests

Unit tests have to test application by not real objects, but just mocks. 
I implemented one mock test in groovy test : 

/app/test/java
 
package com.demien.gradletest
import spock.lang.Specification
class GroovySpockClassTest extends Specification {

    def "Just say hello"() {
        System.out.println("Hello from groovy JUnit test class")
    }

    def "Unit test with mocked method, should return HOW ARE YOU, JOE"() {
        setup :
        TestProcessor mock=Mock()
        mock.getGreetingWord()>>"How are you"        TestClass testClass=new TestClass(testProcessor: mock)

        when:
        String result=testClass.greeting("Joe")

        then:
        result=="How are you, Joe"    }
}

JUnit has no abilities for working with mocks - it's additional libraries like "mockito" 
so JUnit test is "empty" :)  
- it's anoter pointless things - it's also just for using tests in different languages :)
/app/tests/java
package com.demien.gradletest;

import org.junit.Assert;
import org.junit.Test;

public class JavaJunitTestClass {

    @Test    public void itHaveToBeTest() {
        Assert.assertTrue(true);
        System.out.println("Hello from java JUnit test class");
    }
}

9. App - integration test

Integration test have to test application not by mocks(like in unit tests) but by real objects.
So I created 2 tests for testing both implementations of test interface:
 
package com.demien.gradletest
import spock.lang.Specification
class IntegrationTest extends Specification {

    def "Main integration test with REAL HELLO PROCESSOR should return HELLO, JOE"() {

        setup :
        TestProcessor processor=new TestHelloProcessor()
        TestClass testClass=new TestClass(testProcessor: processor)

        when:
        String result=testClass.greeting("Joe")

        then:
        result=="Hello, Joe"    }

    def "Main integration test with REAL HI PROCESSOR should return HI, JOE"() {

        setup :
        TestProcessor processor=new TestHiProcessor()
        TestClass testClass=new TestClass(testProcessor: processor)

        when:
        String result=testClass.greeting("Joe")

        then:
        result=="Hi, Joe"    }


}

10. The end 

Application development is completed. 
We have several sub-projects, different directories for java and groovy and few additional source directories. 
Also, with the help of gradle we now can run integration tests by executing different gradle ask : 


Full source code can be downloaded from here. 

No comments:

Post a Comment