求助:如何在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

