0
votes

In Junit 5 I'm trying to get a test class method to run from an extension. I'm using the Junit 5 extension interface, TestWatcher, and overriding the testFailed() method.

The purpose of this extension is to take a screen shot on failure in the test class's Selenium WebDriver browser and attach it to that test's Allure report. The test class method has the instantiated browser and annotation for attaching to Allure. And my takeScreenshot method relies on the browser and a testName string from the test class to run correctly.

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();
        } 
}

And the code in my test class is something like this:

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;
    }
}

The above test class is able to take a screen shot correctly when calling from @AfterEach in the test method. But I only want to take it on a failure.

When I run the test it calls takeScreenshot, but then gives an exception while executing it:

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$14(TestMethodTestDescriptor.java:299) at org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor.lambda$invokeTestWatchers$3(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$5(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$7(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$8(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$5(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$7(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$8(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$0(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

You can see my logging statement being output before the NullPointerException caused by the next line of code in that method (referencing the driver from the test instance). Is there a correct way to trigger the existing test instance's takeScreenshot() method in context?

OR

If there is a simpler way to take a screen shot on failure directly in the test's @AfterEach method, PLEASE let me know. Seems like a pretty basic use case. :)

2
Wouldn´t it be easier to inject or lookup the WebDriver from the extension and then migrate the screenshot logic to the extension?Glains
You already have the test instance in the variable you call clazz. Why do you try to create a new instance?johanneslink
@johanneslink, Thanks for the hint. If I call takeScreenshot.invoke(clazz); I'm now getting a Selenium exception org.openqa.selenium.NoSuchSessionException. I added some logging and it seems the Junit5 TestWatcher testFailed() method is being invoked after the @AfterEach method in the test where I'm quitting the browser. Seems to me like the TestWatcher methods should be done before any of the cleanup/teardown part of the test lifecycle.Joel Kruse

2 Answers

1
votes

IMO the issue is in the flow that you've described. JUnit creates a new instance of the Test class per test method (although this can be changed).

So much better approach would be:

  1. Make the extension "stateful" in a sense that it will contain the reference to the web Driver.
  2. Do not implement takeScreenshot method in the test, do it in the extension (private method) instead
  3. In the extension implement the callback and "inject" (by reflection) the instance of the WebDriver into the test if you need to use it in the test. This will guarantee that the test runs with the correctly instantiated "state" (instance of webdriver).
  4. In the extension implement the logic of "if the test method failed call the private method of extension takeScreenshot
0
votes

You should not be doing things like instantiating test classes from within extensions, the framework should take care of everything.

Please refer to https://junit.org/junit5/docs/current/user-guide/#extensions 5.9.1 in the documentation, and look at this Q&A

You can either use that, or modify your TestWatcher to do the screenshot as suggested in the comments. You'd have to save your driver reference in the ExtensionContext to be able to access it.