You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Kotlin开发的Android Gradle插件中获取带@Test注解的测试方法

解决Gradle插件中扫描Android项目测试方法的问题

我之前开发Gradle插件时也踩过一模一样的坑——直接用反射根本找不到目标项目的类,因为插件运行在自己的类加载器上下文里,和你要分析的Android项目完全是两个独立的类空间。下面给你几个靠谱的解决思路,按推荐程度排序:

1. 直接解析源码文件(最推荐)

不需要依赖编译后的类,用源码解析库直接读取Java/Kotlin源码的AST(抽象语法树),找出带@Test注解的方法。JavaParser是个很好用的工具,支持Java和Kotlin(Kotlin可以用kotlin-parsers扩展)。

步骤:

  • 给你的Gradle插件项目添加JavaParser依赖:
    dependencies {
        implementation("com.github.javaparser:javaparser-core:3.25.8")
        // 如果要处理Kotlin源码
        implementation("com.github.javaparser:kotlin-parsers:3.25.8")
    }
    
  • 遍历你拿到的ArrayList<File>,逐个解析文件:
    import com.github.javaparser.JavaParser
    import com.github.javaparser.ast.CompilationUnit
    import com.github.javaparser.ast.body.MethodDeclaration
    import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations
    
    fun scanTestMethodsFromSourceFiles(testFiles: List<File>) {
        val javaParser = JavaParser()
        testFiles.forEach { file ->
            val cu: CompilationUnit = if (file.extension == "kt") {
                // 处理Kotlin文件
                com.github.javaparser.kotlin.KotlinParser().parse(file)
            } else {
                javaParser.parse(file)
            }
            // 遍历所有类中的方法
            cu.findAll(MethodDeclaration::class.java).forEach { method ->
                // 检查是否带有@Test注解(注意区分JUnit 4和JUnit 5的全类名)
                val hasTestAnnotation = method.isAnnotationPresent("org.junit.Test") 
                        || method.isAnnotationPresent("org.junit.jupiter.api.Test")
                if (hasTestAnnotation) {
                    println("找到测试方法:${method.nameAsString} 在文件 ${file.name}")
                }
            }
        }
    }
    
    这里要注意区分JUnit 4和JUnit 5的@Test注解全类名,根据你的项目实际情况调整判断逻辑。

2. 解析编译后的字节码

如果你的插件是在测试编译任务之后运行,可以直接读取编译好的字节码文件,用ASM或者ByteBuddy这样的字节码操作库来扫描注解,这种方式比解析源码更准确(能处理编译时生成的代码)。

步骤:

  • 先获取Android项目测试编译任务的输出目录,比如debugTestClasses:
    val androidExtension = project.extensions.getByName("android") as com.android.build.gradle.api.AndroidBasePluginExtension
    val testVariant = androidExtension.testVariants.find { it.name == "debug" }
    val testClassesDir = testVariant?.compileProvider?.get()?.outputs?.files?.firstOrNull()
    
  • 用ASM扫描字节码中的@Test注解:
    import org.objectweb.asm.ClassReader
    import org.objectweb.asm.ClassVisitor
    import org.objectweb.asm.Opcodes
    import java.io.FileInputStream
    
    fun scanTestMethodsFromBytecode(classFiles: List<File>) {
        classFiles.forEach { file ->
            FileInputStream(file).use { inputStream ->
                val classReader = ClassReader(inputStream)
                classReader.accept(object : ClassVisitor(Opcodes.ASM9) {
                    override fun visitMethod(
                        access: Int,
                        name: String?,
                        descriptor: String?,
                        signature: String?,
                        exceptions: Array<out String>?
                    ): org.objectweb.asm.MethodVisitor? {
                        // 检查方法是否带有@Test注解
                        return object : org.objectweb.asm.MethodVisitor(Opcodes.ASM9) {
                            override fun visitAnnotation(desc: String?, visible: Boolean): org.objectweb.asm.AnnotationVisitor? {
                                // 匹配JUnit 4或JUnit 5的@Test注解的内部名称
                                if (desc == "Lorg/junit/Test;" || desc == "Lorg/junit/jupiter/api/Test;") {
                                    println("找到测试方法:$name 在类文件 ${file.name}")
                                }
                                return super.visitAnnotation(desc, visible)
                            }
                        }
                    }
                }, ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
            }
        }
    }
    

3. 动态创建目标项目的类加载器(不推荐)

如果一定要用反射,你需要构建一个包含目标项目测试类和所有依赖的类加载器,然后加载类进行反射。但这种方式非常复杂,因为Android项目的依赖树很庞大,还要处理不同变体的classpath,容易出问题。这里给个简化的示例:

import java.net.URLClassLoader

fun createProjectClassLoader(project: Project, testClassesDir: File): ClassLoader {
    // 收集测试类的classpath,包括项目编译后的类和所有测试依赖
    val classpath = mutableListOf<File>()
    classpath.add(testClassesDir)
    // 添加测试依赖的JAR包
    project.configurations.getByName("debugTestImplementation").files.forEach { classpath.add(it) }
    project.configurations.getByName("debugTestRuntimeOnly").files.forEach { classpath.add(it) }

    val urls = classpath.map { it.toURI().toURL() }.toTypedArray()
    return URLClassLoader(urls, null) // 用null作为父加载器,避免和插件的类加载器冲突
}

// 然后用这个类加载器加载类并反射
fun scanTestMethodsViaReflection(testFiles: List<File>, classLoader: ClassLoader, project: Project) {
    testFiles.forEach { file ->
        // 从文件路径转换为类全限定名,比如com/example/TestClass.java -> com.example.TestClass
        val className = file.absolutePath
            .replace("${project.projectDir.absolutePath}/src/test/java/", "")
            .replace(".java", "")
            .replace(".kt", "")
            .replace("/", ".")
        try {
            val clazz = classLoader.loadClass(className)
            clazz.declaredMethods.forEach { method ->
                if (method.isAnnotationPresent(org.junit.Test::class.java) 
                        || method.isAnnotationPresent(org.junit.jupiter.api.Test::class.java)) {
                    println("找到测试方法:${method.name}")
                }
            }
        } catch (e: ClassNotFoundException) {
            // 处理类找不到的异常,可能是路径转换错误或者依赖缺失
            e.printStackTrace()
        }
    }
}

这种方式需要你准确处理类路径和类名转换,而且如果项目用了Kotlin的一些特性(比如默认方法、内联函数),反射可能会有奇怪的问题,所以除非必要,不建议用。

总结一下,优先用源码解析或者字节码解析的方式,这两种都能避开类加载器的问题,而且更稳定。

内容的提问来源于stack exchange,提问作者rdiaz82

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:07:48