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

如何在Java中为Playwright UI测试实现兼容的重试机制?

问题根源分析

首次测试能正常获取Page参数,但重试时抛出ParameterResolutionException,核心原因是:

  • 第一次执行时,JUnit会自动触发所有注册的扩展(包括Playwright的JUnit扩展)完成参数解析流程,创建并注入有效的Page实例。
  • 自定义的RetryExtension直接调用executableInvoker.invoke()时,没有触发参数解析逻辑,也没有手动获取Playwright的ParameterResolver来生成新的Page实例,导致重试时无法找到参数的解析器。
解决方案

方案1:使用JUnit 5 TestTemplate实现重试(推荐)

JUnit 5的TestTemplate是官方推荐的重试实现方式,每次重试都会触发完整的测试生命周期,包括参数解析,完美适配Playwright的参数注入逻辑。

步骤1:保留原Retry注解

@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    int value() default 5;
}

步骤2:实现TestTemplateInvocationContextProvider扩展

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import java.util.stream.Stream;

public class RetryTemplateExtension implements TestTemplateInvocationContextProvider {

    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return context.getTestMethod()
                .map(method -> method.isAnnotationPresent(Retry.class))
                .orElse(false);
    }

    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
        int retryCount = context.getTestMethod()
                .map(method -> method.getAnnotation(Retry.class).value())
                .orElse(1);
        
        // 生成指定次数的重试上下文,每次调用都会触发完整参数解析
        return Stream.generate(() -> new TestTemplateInvocationContext() {
            @Override
            public String getDisplayName(int invocationIndex) {
                return "Attempt #" + (invocationIndex + 1);
            }
        }).limit(retryCount);
    }
}

步骤3:修改测试类注解

将@Test替换为@TestTemplate,并注册新的重试扩展:

@UsePlaywright(DefaultOptions.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(RetryTemplateExtension.class)
class E2ETest {
    
    @Retry
    @TestTemplate
    void test(Page page) {
        fail();
    }
}

方案2:修改原有扩展,手动解析参数

如果不想改动测试注解(保持@Test),可以修改原RetryExtension,手动获取参数解析器并生成参数后再调用测试方法。

修改后的RetryExtension代码

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

public class RetryExtension implements TestExecutionExceptionHandler {

    private final AtomicInteger counter = new AtomicInteger(1);

    private void printError(Throwable e) {
        System.err.println(
                "Attempt test execution #" + counter.get() +
                        " failed (" + e.getClass().getName() +
                        " thrown):  " + e.getMessage());
    }

    @Override
    public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable {
        printError(throwable);

        Optional<Method> testMethodOpt = extensionContext.getTestMethod();
        if (testMethodOpt.isEmpty()) {
            throw throwable;
        }
        Method method = testMethodOpt.get();
        int maxExecutions = method.isAnnotationPresent(Retry.class) ? method.getAnnotation(Retry.class).value() : 1;

        while (counter.incrementAndGet() <= maxExecutions) {
            try {
                // 手动解析方法参数,触发Playwright的参数解析逻辑
                Object[] params = resolveParameters(extensionContext, method);
                extensionContext.getExecutableInvoker().invoke(method, extensionContext.getRequiredTestInstance(), params);
                return;
            } catch (Throwable t) {
                printError(t);
                if (counter.get() >= maxExecutions) {
                    throw t;
                }
            }
        }
        throw throwable;
    }

    private Object[] resolveParameters(ExtensionContext context, Method method) throws ParameterResolutionException {
        List<Object> params = new ArrayList<>();
        for (java.lang.reflect.Parameter param : method.getParameters()) {
            Object resolvedParam = context.getExtensionRegistry()
                    .stream(ParameterResolver.class)
                    .filter(resolver -> resolver.supportsParameter(param, context))
                    .findFirst()
                    .map(resolver -> resolver.resolveParameter(param, context))
                    .orElseThrow(() -> new ParameterResolutionException(
                            "No ParameterResolver registered for parameter [" + param + "] in method [" + method + "]"));
            params.add(resolvedParam);
        }
        return params.toArray();
    }
}

测试类无需修改,保持原有注解即可。

方案对比
方案优点缺点
TestTemplate实现符合JUnit官方扩展规范,自动处理生命周期和参数解析,代码简洁需要将@Test改为@TestTemplate
手动解析参数无需修改测试注解,兼容原有测试代码结构需要手动处理参数解析逻辑,代码相对复杂,容易遗漏扩展生命周期细节

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 13:44:52