构建脚本测试:TestKit使用、插件功能测试、任务验证、集成测试、持续集成集成
说实话,很多Android开发者写Gradle脚本都是「写完就跑,能编译过就行」。我以前也这样,直到有一次我写了个自定义插件,本地跑得好好的,结果CI上一跑就炸——原来是我依赖了本地环境变量,而CI机器上压根没设这个变量。从那以后,我再也不敢不写测试了。
这一章,我们就来聊聊怎么给Gradle脚本做测试。说白了,就是让你写的插件、任务、配置,都能被自动化验证,而不是靠「人肉测试」。
为什么需要测试Gradle脚本?
你想想看,一个Android项目里,Gradle脚本控制着编译、打包、签名、混淆、多渠道……几乎每个环节。如果脚本出了问题,轻则编译失败,重则打出一个有问题的包。我见过最惨的一次,是某个插件在特定条件下把签名配置覆盖了,结果线上包签名不对,用户装不上——嗯,那场面,相当酸爽。
所以,给Gradle脚本写测试,不是「锦上添花」,而是「雪中送炭」。它能帮你验证:
- 插件能不能正常应用
- 任务能不能正确执行
- 配置参数有没有被正确解析
- 不同Gradle版本下行为是否一致
TestKit:Gradle官方测试框架
Gradle官方提供了一个测试框架,叫TestKit。它专门用来测试Gradle插件和构建逻辑。我个人习惯把它和JUnit或Spock搭配使用。
TestKit的核心思路很简单:它会在一个临时目录里创建一个「迷你项目」,然后在这个项目上运行Gradle构建,最后检查构建结果。说白了,就是模拟真实构建环境。
添加TestKit依赖
首先,在你的插件项目的build.gradle里加上TestKit依赖:
dependencies {
testImplementation gradleTestKit()
testImplementation 'junit:junit:4.13.2'
}
注意,gradleTestKit()这个方法是Gradle自带的,它会自动引入对应版本的TestKit。
第一个TestKit测试
我们来写一个最简单的测试,验证一个插件能不能被成功应用:
import org.gradle.testkit.runner.GradleRunner
import org.junit.Test
import org.junit.Rule
import org.junit.rules.TemporaryFolder
class MyPluginTest {
@Rule
TemporaryFolder testProjectDir = new TemporaryFolder()
@Test
void '插件应该能成功应用'() {
// 准备一个build.gradle文件
def buildFile = testProjectDir.newFile('build.gradle')
buildFile << """
plugins {
id 'com.example.myplugin'
}
"""
// 运行Gradle
def result = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments('tasks')
.withPluginClasspath()
.build()
// 验证结果
assert result.task(':tasks').outcome == TaskOutcome.SUCCESS
}
}
这里有几个关键点:
withPluginClasspath():自动把当前项目的classpath注入到测试项目中,这样插件才能被找到withArguments('tasks'):运行tasks任务,列出所有可用任务build():执行构建,如果失败会抛异常
build()。如果你想测试构建失败的情况,用buildAndFail(),它会返回失败的结果,方便你断言错误信息。
插件功能测试:验证核心逻辑
光验证插件能应用还不够,我们还得验证插件的核心功能。比如,插件注册了一个任务,这个任务能不能正确执行?
假设我们有一个插件,它注册了一个greet任务,输出一句问候语:
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
project.tasks.register('greet') {
doLast {
println "Hello from ${project.name}!"
}
}
}
}
测试代码可以这样写:
@Test
void 'greet任务应该输出问候语'() {
def buildFile = testProjectDir.newFile('build.gradle')
buildFile << """
plugins {
id 'com.example.greeting'
}
"""
def result = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments('greet')
.withPluginClasspath()
.build()
// 检查输出
assert result.output.contains("Hello from test-project!")
}
这里我用result.output来捕获构建过程中的标准输出。如果任务执行了println,输出里就能找到。
System.out的输出。如果你在任务里用了println,它会被捕获。但如果你用了logger.quiet()或logger.lifecycle(),这些也会被捕获。只有System.err需要额外配置。
任务验证:检查任务状态和输出
有时候,我们不光要看输出,还要检查任务的状态。比如,某个任务是不是被跳过了?是不是失败了?
TestKit提供了TaskOutcome枚举,可以检查每个任务的执行结果:
@Test
void 'clean任务应该删除build目录'() {
def buildFile = testProjectDir.newFile('build.gradle')
buildFile << """
plugins {
id 'com.example.myplugin'
}
"""
// 先运行一次,生成build目录
GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments('build')
.withPluginClasspath()
.build()
// 再运行clean
def result = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments('clean')
.withPluginClasspath()
.build()
// 验证clean任务执行成功
def cleanTask = result.task(':clean')
assert cleanTask != null
assert cleanTask.outcome == TaskOutcome.SUCCESS
// 验证build目录被删除
assert !new File(testProjectDir.root, 'build').exists()
}
我个人习惯用result.task(':taskName')来获取具体任务的状态。如果任务不存在,它会返回null,所以记得判空。
集成测试:模拟真实项目
单元测试只能验证插件的基本功能。但真实项目里,插件往往要和其他插件、配置、任务协同工作。这时候就需要集成测试了。
集成测试的思路是:创建一个更完整的「模拟项目」,包含多个模块、多种配置,然后运行完整的构建流程。
举个例子,假设我们写了一个代码生成插件,它会在编译前生成一些Java文件。集成测试可以这样写:
@Test
void '代码生成插件应该能在多模块项目中正常工作'() {
// 创建settings.gradle,包含两个子模块
def settingsFile = testProjectDir.newFile('settings.gradle')
settingsFile << """
rootProject.name = 'test-project'
include 'app'
include 'library'
"""
// 创建app模块的build.gradle
def appDir = new File(testProjectDir.root, 'app')
appDir.mkdirs()
def appBuildFile = new File(appDir, 'build.gradle')
appBuildFile << """
plugins {
id 'com.example.codegen'
id 'java'
}
"""
// 创建library模块的build.gradle
def libDir = new File(testProjectDir.root, 'library')
libDir.mkdirs()
def libBuildFile = new File(libDir, 'build.gradle')
libBuildFile << """
plugins {
id 'com.example.codegen'
id 'java'
}
"""
// 运行编译
def result = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments('compileJava')
.withPluginClasspath()
.build()
// 验证两个模块都生成了代码
assert new File(appDir, 'build/generated/source/codegen').exists()
assert new File(libDir, 'build/generated/source/codegen').exists()
}
集成测试跑起来比较慢,所以我一般只在CI上跑,本地开发时只跑单元测试。
持续集成集成:让测试自动化
测试写好了,怎么让它自动跑起来?当然是集成到CI里。
我常用的CI工具是GitHub Actions和Jenkins。配置起来其实很简单,核心就是让CI执行./gradlew test。
以GitHub Actions为例,一个典型的配置是这样的:
name: Gradle Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Run tests
run: ./gradlew test --no-daemon
这里有几个要点:
--no-daemon:在CI上建议关闭Gradle守护进程,避免资源占用问题--stacktrace:如果测试失败,打印完整的堆栈信息,方便排查- 缓存
.gradle目录:可以加速后续构建
withPluginClasspath(),不要依赖外部环境。
知识体系总览
下面这张图,把本章的核心知识点串起来了:
常见问题与避坑指南
最后,分享几个我踩过的坑:
- TestKit版本不匹配:我曾经用Gradle 7.0的TestKit去测试一个为Gradle 6.x写的插件,结果一堆API找不到。后来我养成了一个习惯:测试用的Gradle版本,一定要和插件目标版本一致。可以用
withGradleVersion('6.9')指定版本。 - 临时目录清理:TestKit默认不会清理临时目录。如果测试跑多了,磁盘上会留下一堆垃圾。我一般会在
@After方法里手动清理,或者用TemporaryFolder的delete()方法。 - 测试速度:每个TestKit测试都要启动一个Gradle进程,速度很慢。我建议把测试分组,单元测试用JUnit跑,集成测试单独用一个task跑,只在CI上执行。
嗯,关于构建脚本测试,就聊这么多。记住一句话:测试不是负担,而是保护网。有了它,你改脚本的时候才敢放心大胆地重构。