Joel Kruse
Joel Kruse

Reputation: 41

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

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. :)

Upvotes: 0

Views: 2840

Answers (3)

Joel Kruse
Joel Kruse

Reputation: 41

The solution ended up looking like this. You could add other actions here for a Selenium test because this executes just before test tear-down.

If you are using Junit5 for Selenium testing you can use the AfterTestExecutionCallback so that the RequiredTestInstance contains both the reference to the browser AND the final result of the test!

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

Upvotes: 1

Mark Bramnik
Mark Bramnik

Reputation: 42511

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

Upvotes: 1

Vangelisz Ketipisz
Vangelisz Ketipisz

Reputation: 967

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.

Upvotes: 0

Related Questions