如何确保@Timeout超时后仍能执行Groovy测试的清理代码?
解决Spock测试超时后Cleanup块不执行的问题
这个问题我之前也碰到过——Spock原生的@Timeout注解会直接终止测试线程,导致cleanup块里的日志压缩、Slack通知这些关键逻辑根本没机会运行。要解决这个问题,核心是替换原生超时机制,改用可控的线程超时管理,确保无论测试是否超时,清理逻辑都能被触发。下面是两种实用方案:
方案一:自定义线程超时(快速解决单个测试)
直接移除测试方法上的@Timeout(80),用ExecutorService手动管理测试逻辑的超时,同时让测试代码响应线程中断,保证流程能走到cleanup块。
步骤1:修改测试方法,用线程池托管测试逻辑
def 'Start_test'() { setup: // 创建单线程池执行测试核心逻辑 def executor = Executors.newSingleThreadExecutor() def testFuture = executor.submit { // 原测试的setup/when/and/then逻辑全部放在这里 setup: // 你的初始化操作 when: 'Test_started' // 你的测试执行操作 and: 'Check_something' // 你的中间检查操作 then: 'Validate_something' checkConnectedProducts() } when: try { // 等待测试执行,设置80秒超时阈值 testFuture.get(80, TimeUnit.SECONDS) } catch (TimeoutException e) { // 超时后中断测试线程,触发中断响应逻辑 testFuture.cancel(true) // 抛出超时异常,不影响后续cleanup执行 throw new AssertionError("Test timed out after 80 seconds", e) } catch (ExecutionException e) { // 转发测试逻辑内部抛出的异常 throw e.cause } finally { // 关闭线程池,释放资源 executor.shutdownNow() } cleanup: // 原清理逻辑保持不变,现在一定会执行 File log = new File("$deploymentPath/logs") def errorCount = countError() def zipFileName = "xxx.zip" IOHelper.createZip(log, zipFileName) File zipFile = new File(zipFileName) ZipUploader.Slack(zipFile, errorCount) }
步骤2:让检查逻辑响应线程中断
你的checkConnectedProducts()方法里有Thread.sleep(1000),线程被中断时会抛出InterruptedException,需要处理这个异常,确保线程能优雅退出循环:
void checkConnectedProducts(){ def productConnected = XXXhelper.countConnectedProducts() while (productConnected > 2) { try { Thread.sleep(1000) } catch (InterruptedException e) { // 恢复线程中断状态,让上层感知中断 Thread.currentThread().interrupt() // 退出循环,终止检查逻辑 break } productConnected = XXXhelper.countConnectedProducts() } }
方案二:自定义Spock扩展(复用性更强)
如果多个测试类都需要这个功能,可以封装成Spock扩展,避免重复代码。
步骤1:实现Spock拦截器接口
import org.spockframework.runtime.extension.IMethodInterceptor import org.spockframework.runtime.extension.IMethodInvocation import java.util.concurrent.* class TimeoutWithCleanupExtension implements IMethodInterceptor { private final int timeoutSeconds TimeoutWithCleanupExtension(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds } @Override void intercept(IMethodInvocation invocation) throws Throwable { ExecutorService executor = Executors.newSingleThreadExecutor() Future<?> future = executor.submit { // 执行完整测试流程(setup+测试逻辑+cleanup) invocation.proceed() } try { future.get(timeoutSeconds, TimeUnit.SECONDS) } catch (TimeoutException e) { future.cancel(true) throw new AssertionError("Test timed out after ${timeoutSeconds} seconds", e) } catch (ExecutionException e) { throw e.cause } finally { executor.shutdownNow() } } }
步骤2:创建自定义注解
import org.spockframework.runtime.extension.ExtensionAnnotation import java.lang.annotation.* @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.TYPE, ElementType.METHOD]) @ExtensionAnnotation(TimeoutWithCleanupExtension.class) @interface TimeoutWithCleanup { int value() default 60 // 默认超时60秒 }
步骤3:在测试中使用注解
class StartupTest { // ... @TimeoutWithCleanup(80) def 'Start_test'() { // 原测试逻辑保持不变 setup: // 初始化操作 when: 'Test_started' // 测试操作 and: 'Check_something' // 检查操作 then: 'Validate_something' checkConnectedProducts() cleanup: // 清理逻辑保持不变 File log = new File("$deploymentPath/logs") def errorCount = countError() def zipFileName = "xxx.zip" IOHelper.createZip(log, zipFileName) File zipFile = new File(zipFileName) ZipUploader.Slack(zipFile, errorCount) } }
同样,别忘了修改checkConnectedProducts()方法来响应中断。
核心原理
两种方案都避免了Spock原生超时的强制线程终止,改用线程中断控制超时逻辑。线程中断不会直接杀死线程,而是给线程发送中断信号,让代码有机会执行收尾逻辑,最终保证cleanup块能正常运行。
内容的提问来源于stack exchange,提问作者Ju3332
相关产品推荐
相关产品推荐

