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

求助:如何在IntelliJ插件中提取结构化单元测试数据

求助:如何在IntelliJ插件中提取结构化单元测试数据

我之前在开发IntelliJ插件处理测试结果时,也碰到过几乎一模一样的问题——SMTestProxy的方法返回null、拿不到测试元数据、PSI解析失败这些坑真的挺头疼的!结合我的踩坑经验,给你几个可行的解决方案:

1. 先确认你在处理正确的测试事件节点

你目前只监听了testSuiteFinished事件,但这个事件对应的通常是根测试套件(name为[root]),本身不包含单个测试用例的具体数据。单个测试方法的结果要在testFinished事件中处理,同时需要递归遍历测试树的所有子节点,才能拿到完整的测试层级结构。

另外,要注意区分复合节点(测试套件)和叶子节点(单个测试用例):SMTestProxy的isComposite()方法返回true时表示这是一个测试套件,需要继续遍历其子节点;返回false时才是单个测试用例,包含具体的测试名、状态、失败信息等。

2. 用SMTestInfo直接提取元数据(比PSI更可靠)

你之前尝试用getLocation()解析PSI元素失败,很大概率是因为测试是通过Gradle等构建工具运行的,SMTestProxy的位置信息和IntelliJ的PSI索引不同步。这时候可以直接通过SMTestProxy.getDescriptor()获取SMTestInfo对象,它能直接返回全限定类名、测试方法名等核心元数据,不需要依赖PSI解析:

val testInfo = testProxy.descriptor as? SMTestInfo
val qualifiedClassName = testInfo?.className // 全限定类名
val methodName = testInfo?.methodName // 测试方法名

3. 正确提取失败测试的栈追踪

只有单个测试用例(非复合节点)才会包含失败信息,你可以通过testProxy.errorMessages获取所有错误详情,其中每个SMTestError对象都包含完整的栈追踪字符串。如果需要清理栈追踪,可以过滤掉框架无关的行:

if (testProxy.status == TestStatus.FAILED) {
    testProxy.errorMessages.forEach { error ->
        val cleanStackTrace = error.stackTrace?.lines()
            ?.filterNot { it.contains("junit.framework") || it.contains("org.junit.runner") }
            ?.joinToString("\n")
        log.info("Clean failure stack trace:\n$cleanStackTrace")
    }
}

4. 处理PSI解析的延迟问题

如果你确实需要PSI元素(比如获取测试代码的具体内容),要注意IntelliJ的索引可能还未完成,需要用DumbService等待索引就绪后再解析:

project?.let { proj ->
    DumbService.getInstance(proj).runWhenSmart {
        testProxy.location?.psiElement?.let { psi ->
            when (psi) {
                is PsiMethod -> log.info("Resolved test method: ${psi.containingClass?.qualifiedName}#${psi.name}")
                is PsiClass -> log.info("Resolved test class: ${psi.qualifiedName}")
            }
        }
    }
}

修改后的完整Listener示例

class MyTestStatusListener : TestStatusListener() {
    private val log = thisLogger()

    override fun testFinished(testProxy: AbstractTestProxy?, project: Project?) {
        if (testProxy !is SMTestProxy) {
            log.info("Skipping non-SMTestProxy test finished event")
            return
        }
        // 处理单个测试用例
        if (!testProxy.isComposite) {
            processSingleTestCase(testProxy, project)
        }
    }

    override fun testSuiteFinished(testProxy: AbstractTestProxy?, project: Project?) {
        if (testProxy !is SMTestProxy) {
            log.info("Skipping non-SMTestProxy test suite finished event")
            return
        }
        // 遍历整个测试树,确保所有测试用例都被处理
        traverseTestTree(testProxy, project)
    }

    private fun traverseTestTree(testProxy: SMTestProxy, project: Project?) {
        if (testProxy.isComposite) {
            testProxy.children.forEach { child ->
                if (child is SMTestProxy) traverseTestTree(child, project)
            }
        } else {
            processSingleTestCase(testProxy, project)
        }
    }

    private fun processSingleTestCase(testProxy: SMTestProxy, project: Project?) {
        val testInfo = testProxy.descriptor as? SMTestInfo
        val fullTestName = when {
            testInfo != null && testInfo.className != null && testInfo.methodName != null ->
                "${testInfo.className}#${testInfo.methodName}"
            else -> testProxy.name ?: "Unknown test"
        }

        val status = testProxy.status
        val duration = testProxy.duration
        log.info("Test completed: $fullTestName | Status: $status | Duration: ${duration}ms")

        // 提取失败信息
        if (status == TestStatus.FAILED || status == TestStatus.ERROR) {
            testProxy.errorMessages.forEach { error ->
                log.info("Failure message: ${error.message}")
                error.stackTrace?.let { stackTrace ->
                    val cleanStackTrace = stackTrace.lines()
                        .filterNot { it.contains("junit.framework") || it.contains("org.junit.runner") }
                        .joinToString("\n")
                    log.info("Clean stack trace:\n$cleanStackTrace")
                }
            }
        }

        // 可选:解析PSI元素
        project?.let { proj ->
            DumbService.getInstance(proj).runWhenSmart {
                testProxy.location?.psiElement?.let { psi ->
                    when (psi) {
                        is PsiMethod -> log.info("Resolved to method: ${psi.containingClass?.qualifiedName}#${psi.name}")
                        is PsiClass -> log.info("Resolved to class: ${psi.qualifiedName}")
                    }
                }
            }
        }
    }
}

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:04:31