英文:
In Junit 5 how can I call a test class method from an extension?
问题
在Junit 5中,我尝试从一个扩展(extension)中运行测试类方法。我正在使用Junit 5的扩展接口TestWatcher,并重写testFailed()方法。
这个扩展的目的是在测试类的Selenium WebDriver浏览器中出现失败时,拍摄屏幕截图,并将其附加到该测试的Allure报告中。测试类方法具有实例化的浏览器和附加到Allure的注解。而我的takeScreenshot方法依赖于浏览器和来自测试类的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();
}
}
}
而在我的测试类中的代码类似于这样:
package tests;
@ExtendWith(ScreenshotOnFailureExtension.class)
public class MyTest implements Config {
public WebDriver driver;
public String testName;
//一堆Junit5注解,用于初始化上述变量...
//拍摄屏幕截图
public void takeScreenshot() {
System.out.println("Taking screenshot.");
byte[] srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
saveScreenshot(srcFile, testName + ".png");
}
//这将截图附加到Allure测试结果
@Attachment(value = "{testName}", type = "image/png")
public byte[] saveScreenshot(byte[] screenShot, String testName) {
System.out.println("Attaching screenshot to Allure report");
return screenShot;
}
}
上述测试类可以在测试方法中的@AfterEach中正确地拍摄截图。但我只想在失败时进行拍摄。
当我运行测试时,它调用了takeScreenshot,但在执行它时引发了异常:
Taking screenshot.java.lang.reflect.InvocationTargetException
...
你可以看到在NullPointerException之前,我的日志记录语句被输出,这是由该方法中的下一行代码(引用测试实例的driver
)引起的。有没有一种正确的方法在上下文中触发现有测试实例的takeScreenshot()
方法?
或者
如果有一种更简单的方法在测试的@AfterEach
方法中直接在失败时拍摄屏幕截图,请告诉我。这似乎是一个非常基本的用例。
英文:
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.
答案1
得分: 1
IMO,问题出在您所描述的流程中。
JUnit 每个测试方法都会创建 Test 类的新实例(尽管可以更改此行为)。
因此,更好的方法是:
- 使这个扩展“有状态”,意味着它将包含对 Web 驱动程序的引用。
- 不要在测试中实现
takeScreenshot
方法,而是在扩展中(私有方法中)实现它。 - 在扩展中实现回调,并通过反射将 WebDriver 的实例“注入”到测试中,如果您需要在测试中使用它。这将确保测试在正确实例化的“状态”(webdriver 实例)下运行。
- 在扩展中实现“如果测试方法失败,则调用扩展的私有方法
takeScreenshot
的逻辑。
英文:
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:
- Make the extension "stateful" in a sense that it will contain the reference to the web Driver.
- Do not implement
takeScreenshot
method in the test, do it in the extension (private method) instead - 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).
- In the extension implement the logic of "if the test method failed call the private method of extension
takeScreenshot
答案2
得分: 0
你不应该在扩展内部实例化测试类,框架应该处理一切。
请参考文档中的 https://junit.org/junit5/docs/current/user-guide/#extensions 5.9.1,并查看此问答。
你可以选择使用那个方法,或者根据评论中的建议修改你的 TestWatcher 以执行截图操作。你需要将驱动程序的引用保存在 ExtensionContext 中以便访问。
英文:
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.
答案3
得分: 0
以下是翻译好的代码部分:
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 {
// 如果上下文中存在 ExecutionException,则测试失败
Boolean testFailed = context.getExecutionException().isPresent();
if (testFailed) {
// 通过 Java 反射进行截屏
try {
Object clazz = context.getRequiredTestInstance();
Method takeScreenshot = clazz.getClass().getMethod("takeScreenshot");
// 'takeScreenshot' 是我的测试类中的一个方法
// 它使用 Selenium 驱动程序进行截屏
// 然后将截图附加到 Allure 报告中
takeScreenshot.setAccessible(true);
takeScreenshot.invoke(clazz);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
英文:
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();
}
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论