在 Junit 5 中,如何从扩展中调用测试 class 方法?

In Junit 5 how can I call a test class method from an extension?

在 Junit 5 中,我试图从扩展程序中获取 class 到 运行 的测试方法。我正在使用 Junit 5 扩展接口 TestWatcher,并重写了 testFailed() 方法。

此扩展的目的是在测试 class 的 Selenium WebDriver 浏览器中拍摄失败的屏幕截图,并将其附加到该测试的 Allure 报告中。测试 class 方法具有实例化的浏览器和用于附加到 Allure 的注释。我的 takeScreenshot 方法依赖于浏览器和从测试 class 到 运行 的 testName 字符串正确。

package utils;

public class ScreenshotOnFailureExtension implements TestWatcher{
    @Override
    public void testFailed(ExtensionContext context, Throwable cause) {
        try {
            Object clazz = context.getRequiredTestInstance();
            Method takeScreenshot = clazz.getClass().getMethod("takeScreenshot");
            takeScreenshot.setAccessible(true);
            Object test = clazz.getClass().getConstructor().newInstance();
            takeScreenshot.invoke(test);
        } catch (Exception e) {
            e.printStackTrace();
        } 
}

我测试中的代码 class 是这样的:

package tests;

@ExtendWith(ScreenshotOnFailureExtension.class)
public class MyTest implements Config {
    public WebDriver driver;
    public String testName;

//bunch of Junit5 annotations with functions to initialize above variables omitted...

    //take a screen shot
    public void takeScreenshot() {
        System.out.println("Taking screenshot.");
        byte[] srcFile=((TakesScreenshot)driver).getScreenshotAs(OutputType.BYTES);
        saveScreenshot(srcFile, testName+ ".png");
    }
    
    //this attaches screenshot to an allure test result
    @Attachment(value = "{testName}", type = "image/png")
    public byte[] saveScreenshot(byte[] screenShot, String testName) {
        System.out.println("Attaching screenshot to Allure report");
        return screenShot;
    }
}

上面的测试class在测试方法中从@AfterEach调用时能够正确截屏。但我只想接受失败。

当我 运行 测试时它调用了 takeScreenshot,但在执行它时出现异常:

Taking screenshot.java.lang.reflect.InvocationTargetException

at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at utils.ScreenshotOnFailureExtension.testFailed(ScreenshotOnFailureExtension.java:49) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$nodeFinished(TestMethodTestDescriptor.java:299) at org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor.lambda$invokeTestWatchers(MethodBasedTestDescriptor.java:134) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor.invokeTestWatchers(MethodBasedTestDescriptor.java:132) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.nodeFinished(TestMethodTestDescriptor.java:290) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.nodeFinished(TestMethodTestDescriptor.java:65) at org.junit.platform.engine.support.hierarchical.NodeTestTask.reportCompletion(NodeTestTask.java:176) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:89) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute[=16=](EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75) at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:89) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209) Caused by: java.lang.NullPointerException at tests.Base.takeScreenshot(Base.java:240) ... 49 more

您可以看到我的日志语句在该方法的下一行代码(引用测试实例中的 driver)导致的 NullPointerException 之前输出。是否有正确的方法在上下文中触发现有测试实例的 takeScreenshot() 方法?

如果有更简单的方法直接在测试的 @AfterEach 方法中拍摄失败的屏幕截图,请告诉我。似乎是一个非常基本的用例。 :)

您不应该在扩展中执行实例化测试 类 之类的操作,框架应该会处理所有事情。

请参考https://junit.org/junit5/docs/current/user-guide/#extensions 5.9.1 in the documentation, and look at

您可以使用它,也可以修改您的 TestWatcher 以按照评论中的建议进行屏幕截图。您必须将驱动程序引用保存在 ExtensionContext 中才能访问它。

IMO 问题出在您描述的流程中。 JUnit 为每个测试方法创建一个新的测试实例 class(尽管这可以更改)。

更好的方法是:

  1. 在某种意义上使扩展“有状态”,它将包含对 Web 驱动程序的引用。
  2. 不要在测试中实现 takeScreenshot 方法,而是在扩展(私有方法)中实现
  3. 在扩展中实现回调并将 WebDriver 的实例“注入”(通过反射)到测试中(如果您需要在测试中使用它)。这将保证测试以正确实例化的“状态”(webdriver 实例)运行。
  4. 在扩展中实现“如果测试方法失败则调用扩展的私有方法”的逻辑takeScreenshot

解决方案最终看起来像这样。您可以在此处为 Selenium 测试添加其他操作,因为它会在测试 tear-down.

之前执行

如果您使用 Junit5 进行 Selenium 测试,您可以使用 AfterTestExecutionCallback,以便 RequiredTestInstance 包含对浏览器的引用和测试的最终结果!

package utils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

public class ActionsOnFailureExtension implements AfterTestExecutionCallback {

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        // if an ExecutionException is part of the context then the test failed
        Boolean testFailed = context.getExecutionException().isPresent();
        if (testFailed) {
            // take a screenshot via Java reflection
            try {
                Object clazz = context.getRequiredTestInstance();
                Method takeScreenshot = clazz.getClass().getMethod("takeScreenshot");
                // 'takeScreenshot' is a method in my test class
                // that uses the Selenium driver to take the screenshot
                // and then attaches it to the Allure report
                takeScreenshot.setAccessible(true);
                takeScreenshot.invoke(clazz);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}