NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

Gradle深入与实战(四)自定义集成测试任务

| Comments

由于本小节,涉及到自定义任务,所以穿插一点自定义任务的知识。

Gradle Task

在前面已经介绍过Gradle和Ant相似,由任务驱动,以任务依赖的方式形成任务链,从而实现构建生命周期。所以,任务是Gradle中一个完整的可执行单元。

如何定义任务:

1
2
3
task hello {
    println 'hello Gradle'
}

执行该任务,只需要输入命令gradle hello。定义task的方式有很多种:

1
2
3
4
5
task myTask
task myTask { configure closure }
task myType << { task action }
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }

其中有一种定义方式,传入了一个参数type,作用是预定义该task的类型,指定类型之后,在传入的闭包中就可以使用该类型task提供的特殊变量或函数。

比如一个拷贝类型的task

1
2
3
4
task copyDocs(type: Copy) {
    from 'src/main/doc'
    into 'build/target/doc'
}

更过关于Task的内容,在以后的章节中再介绍。

自定义集成测试任务

现在我们开始写一个集成测试的task,需求是这样的:

作为一个Java的程序员,我想要将单元测试和集成测试分离

1.我想要 将单元测试全部放在src/test/unit目录中,将集成测试全部放在src/test/intgetaion中
2.我想要 能够单独运行我的集成测试
3.我想要 在运行build命令时,同时跑单元测试和集成测试

根据这样的一个需求,划分几步来做:
1.建立目录
2.目录结构已经和原来的默认规约不同,所以要更改Java插件提供的SourceSet test,来映射单元测试目录结构
3.需要新建一个SourceSet intTest,来映射集成测试目录结构
4.Java插件会给新建的SourceSet intTest定义两个Configuration,分别是intTestCompile和intTestRuntime,那么就需要给这两个分组指定构件内容和依赖
5.定义一个名字叫做integrationTest的测试的task

那么我们从第二步和第三步开始,修改Java插件提供的SourceSet test和新建SourceSet intTest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义一些常量,在其他位置使用
ext {
    unitJavaSrcDir = 'src/test/unit/java'
    unitResourcesSrcDir = 'src/test/unit/resources'
    intJavaSrcDir = 'src/test/integration/java'
    intResourcesSrcDir = 'src/test/integration/resources'
}

sourceSets {
    test {
        java {
            srcDir unitJavaSrcDir
        }
        resources {
            srcDir unitResourcesSrcDir
        }
    }
    intTest {
        java {
            srcDir intJavaSrcDir
        }
        resources {
            srcDir intResourcesSrcDir
        }
    }
}

第三步,给intTestCompile和intTestRuntime指定指定构件内容(产品代码)和依赖

1
2
3
4
5
6
7
dependencies {
    testCompile 'junit:junit:4.11'
    testCompile 'org.mockito:mockito-core:1.9.5'

    intTestCompile sourceSets.main.output // 将sourceSets.main中的输出class指定到intTestCompile中
    intTestCompile configurations.testCompile // 将configurations.testCompile的依赖拿过来
}

最后一步,定义一个test类型的task,并让check任务依赖于它

1
2
3
4
5
6
task integrationTest(type: Test) {
    testClassesDir = sourceSets.intTest.output.classesDir
    classpath = sourceSets.intTest.runtimeClasspath
}

check.dependsOn integrationTest

然后,你就可以在命令行中运行gradle integrationTest。

完整版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
apply plugin: 'java'
apply plugin: 'idea'

ext {
    unitJavaSrcDir = 'src/test/unit/java'
    unitResourcesSrcDir = 'src/test/unit/resources'
    intJavaSrcDir = 'src/test/integration/java'
    intResourcesSrcDir = 'src/test/integration/resources'
}

sourceSets {
    test {
        java {
            srcDir unitJavaSrcDir
        }
        resources {
            srcDir unitResourcesSrcDir
        }
    }
    intTest {
        java {
            srcDir intJavaSrcDir
        }
        resources {
            srcDir intResourcesSrcDir
        }
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.11'
    testCompile 'org.mockito:mockito-core:1.9.5'

    intTestCompile sourceSets.main.output
    intTestCompile configurations.testCompile
}

task integrationTest(type: Test) {
    testClassesDir = sourceSets.intTest.output.classesDir
    classpath = sourceSets.intTest.runtimeClasspath
}

check.dependsOn integrationTest

idea {
    module {
        testSourceDirs += file(unitJavaSrcDir)
        testSourceDirs += file(unitResourcesSrcDir)
        testSourceDirs += file(intJavaSrcDir)
        testSourceDirs += file(intResourcesSrcDir)
    }
}

参考资料:
1.Gradle官方文档
2.http://selimober.com/blog/2014/01/24/separate-unit-and-integration-tests-using-gradle/

Gradle深入与实战(三)依赖管理工具

| Comments

大部分的项目都不是自包含的,也就是说,需要使用到其他项目的构建结果,比如一些Jar文件。它们作为输入文件,必须存在于项目的ClassPath下,程序才能编译和运行。这些输入文件有一个很表意的名字,叫做依赖。

Gradle允许你告诉它项目的依赖是什么,然后它就会负责找到这些依赖。这些依赖会从Maven或者Ivy的远程仓库下载下来(大部分情况),并缓存在本地的某个路径,这个过程叫做依赖解析。

Maven和Gradle一样也提供了类似的功能,而Ant没有,你只能告诉Ant依赖文件的相对或者绝对路径,让它去加载。

常常一个依赖自己也存在依赖,我们称为传递依赖,依赖管理工具又具有解析传递依赖的能力。

Gradle的依赖管理

那么如何在Gradle中定义依赖呢?看个最简单的例子。

1
2
3
4
5
6
7
8
9
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.11'// testCompile group: 'junit', name: 'junit', version: '4.11'
}

项目使用了Java的插件,在repositories块中告诉Gradle使用maven的远程仓库作为依赖下载地址,在dependencies块定义了一个junit的依赖,并说明了分组(Maven中的Scope),后面注释中有一个表意更完整的依赖定义,说明了依赖声明使用的三个坐标group,name,version。

整个看起来是那么的表意,使用过Maven更会觉得是无缝转换,甚至更简洁。

Dependency configurations 依赖分组

在Gradle中,依赖都被会分配到某一个具体的configuration中(这里我不倾向于翻译成配置,我觉得布局,或者分组更适合)。Configuration代表着一个或多个构件及构件所需依赖的一个分组。

Java插件已经预定义了一些configuration,比如,compile,runtime,testCompile,testRuntime等。

compile 放在这个configuration下的依赖是在编译产品代码时所使用的,但它作为一个分组,包含产品代码和编译所需的依赖。
runtime 产品代码在运行时需要的依赖,默认,也会包含compile中的依赖。
testCompile 编译测试代码时所需要的依赖,默认,被编译的产品代码和产品代码需要的编译依赖也属于该分组。
testRuntime 运行测试时需要的依赖。默认,包含compile,runtime和testCompile的分组的构建和依赖。

使用过Maven的都应该知道分组的含义,这里讲解给不明白的同学,依赖之所以要分组,是因为,每个阶段对依赖的需要不一样,最明显的是产品代码和测试代码,比如junit在产品代码中就不需要。

那么,为什么产品代码的编译阶段和运行阶段也分组,一般编译阶段需要的依赖,在运行阶段也需要,但是反过来就不一定了。比如,你通过反射去load一个class,这时该class就不一定需要在编译阶段存在。

一个更常见的例子,做web开发时需要servlet的依赖,但是只是编译阶段,运行时servlet依赖由servlet容器来提供。所以Gradle的War插件也提供了两个configuration,分别是providedCompile和providedRuntime,它们对依赖的使用范围定义和compile以及runtime一致,只不过依赖的Jar包不会被加到War包里面。

定义SourceSet时,添加的Configuration

上一节,在介绍Java插件的时候,提到了SourceSet概念。针对每一个新添加的SourceSet,Java插件都会动态的给它添加两个Configuration,分别是sourceSetCompile和sourceSetRuntime。

比如:新添加一个SourceSet,叫做int,那么对应的Configuration是intCompile和intRuntime。

这一特性也正好印证,Java插件是如何识别自定义SourceSet来进行编译和运行。

依赖的多种定义方式

除了通过远程仓库和依赖坐标来定义依赖,Gradle还提供了另外两种常用的依赖定义方式,对本地文件的依赖,对某个项目的依赖。

对文件的依赖

这种情况看起来是不是很奇葩,都有依赖管理了和Maven仓库了还要什么文件依赖。其实不然,使用这种定义方式,最常见场景是项目构建工具的迁移,从Ant到Gradle。无论任何项目,迁移过程都是小步前进,Gradle提供文件依赖的配置,就是为了解决这些特殊性。

1
2
3
4
dependencies {
    runtime files('libs/a.jar', 'libs/b.jar')
    runtime fileTree(dir: 'libs', include: '*.jar')
}

对另一个工程的依赖

项目中划分子模块是很平常的事情,前端Controller和数据层Dao分离管理就是一个例子,那么在进行前端Controller模块构建时,就需要将数据层模块作为依赖。定义方式如下:

1
2
3
dependencies {
    compile project(':shared')
}

依赖版本冲突

依赖冲突是所以依赖管理中最头痛的问题,这常常出现在传递依赖中。Gradle对解决传递依赖提供了两种策略,使用最新版本或者直接导致构建失败。默认的策略是使用最新版本。虽然这样的策略能够解决一些问题,但是还是不够。常见的一种情况是,NoSuchMethond或者ClassNotFound。这时候,你可能需要一些特殊手段,比如排除不想要的传递依赖。

排除传递依赖

排除传递依赖有多种原因,远程仓库中不存在,运行时不需要,或者版本冲突。排除传递依赖的方式有两种:1.直接在configuration中排除 2.在具体的某个dependency中排除

1
2
3
4
5
6
7
8
9
10
configurations {
    compile.exclude module: 'commons'
    all*.exclude group: 'org.gradle.test.excludes', module: 'reports'
}

dependencies {
    compile("org.gradle.test.excludes:api:1.0") {
        exclude module: 'shared'
    }
}

通过命令行查看依赖关系

当出现依赖冲突时,最主要的还是要分析依赖冲突的原因,Gradle提供了两个任务来帮助你分析依赖关系

dependencies - Displays all dependencies declared in root project ‘projectReports’.
dependencyInsight - Displays the insight into a specific dependency in root project ‘projectReports’.

Tips:输出依赖关系图到文件

在命令行中直接使用gradle dependencies可以打印出依赖图,但是在命令行中查看始终不太方便,我们可以将结果输出到一个文件中,如下:

1
gradle dependencies > dependencies.txt

dependencies.txt保存在项目的根目录

Gradle的官方文档中关于Gradle的依赖管理的内容还有很多,比如,如何访问需要用户名密码授权的Maven仓库等等。等多内容,可以参考官方文档:http://gradle.org/docs/current/userguide/dependency_management.html

下一节,利用前三节学到的知识,编写集成测试任务,并单独划分SourceSet。

参考资料:
1.Gradle官方文档