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

如何确保@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 21:02:52