简体   繁体   English

如何获取在 Java 中 Cucumber 测试失败时抛出的异常?

[英]How to get the exception that was thrown when a Cucumber test failed in Java?

I can perform actions on test failure by using:我可以使用以下方法对测试失败执行操作:

@After
public void afterTest(Scenario scenario) {
    if (scenario.isFailed()) {
        /*Do stuff*/
    }
}

However some of the actions I need to perform depend on the Exception that was thrown and in what context it was thrown.但是,我需要执行的某些操作取决于抛出的异常以及抛出的上下文。 Is there a way to get the Throwable that caused the test to fail?有没有办法获得导致测试失败的 Throwable? For example in JUnit I would do this by extending TestWatcher and add a rule to my tests:例如在 JUnit 中,我会通过扩展 TestWatcher 并在我的测试中添加规则来做到这一点:

@Override
protected void failed(Throwable e, Description description) {
    /*Do stuff with e*/
}

However the cucumber-junit iplementation does not allow the use of rules, so this solution would not work with Cucumber.但是,cucumber-junit 实施不允许使用规则,因此该解决方案不适用于 Cucumber。

I don't think I need to explain why accessing a thrown exception on test failure would be useful, however I will still provide an Example:我认为我不需要解释为什么在测试失败时访问抛出的异常会很有用,但是我仍然会提供一个示例:

My test environment is not always stable, so my tests might fail unexpectedly at any moment (there's no specific place I can try to catch the exception since it could occur at any time).我的测试环境并不总是稳定的,所以我的测试可能在任何时候意外失败(没有特定的地方我可以尝试捕获异常,因为它随时可能发生)。 When this happens I need the test to reschedule for another attempt, and log the incident so that we can get some good statistical data on the environment instability (when, how frequent, how long etc.)发生这种情况时,我需要重新安排测试以进行另一次尝试,并记录事件,以便我们可以获得一些关于环境不稳定的良好统计数据(何时、多频繁、多长时间等)

I've implemented this method using reflections.我已经使用反射实现了这个方法。 You can't access directly to steps errors (stack trace).您无法直接访问步骤错误(堆栈跟踪)。 I've created this static method which allows you to access to "stepResults" attribute and then you can iterate and get the error and do whatever you want.我创建了这个静态方法,它允许您访问“stepResults”属性,然后您可以迭代并获取错误并做任何您想做的事情。

import cucumber.runtime.ScenarioImpl; 
import gherkin.formatter.model.Result;  
import org.apache.commons.lang3.reflect.FieldUtils;  
import java.lang.reflect.Field;  
import java.util.ArrayList;

@After
public void afterScenario(Scenario scenario) {
  if (scenario.isFailed())
    logError(scenario);
}


private static void logError(Scenario scenario) {
   Field field = FieldUtils.getField(((ScenarioImpl) scenario).getClass(), "stepResults", true);
   field.setAccessible(true);
   try {
       ArrayList<Result> results = (ArrayList<Result>) field.get(scenario);
       for (Result result : results) {
           if (result.getError() != null)
               LOGGER.error("Error Scenario: {}", scenario.getId(), result.getError());
       }
   } catch (Exception e) {
       LOGGER.error("Error while logging error", e);
   }
}

The problem with the work around suggested by Frank Escobar: Frank Escobar 建议的解决方法的问题:

By using reflection to reach into a frameworks internals you're depending on implementation details.通过使用反射进入框架内部,您依赖于实现细节。 This is a bad practice, when ever the framework changes its implementation your code may break as you will observe in Cucumber v5.0.0.这是一种不好的做法,当框架更改其实现时,您的代码可能会中断,正如您在 Cucumber v5.0.0 中所观察到的那样。

Hooks in Cucumber are designed to manipulate the test execution context before and after a scenario. Cucumber 中的 Hook 旨在在场景之前和之后操纵测试执行上下文。 They're not made to report on the test execution itself.他们不是为了报告测试执行本身。 Reporting is cross cutting concern and best managed by using the plugin system.报告是交叉关注的问题,最好使用插件系统进行管理。

For example:例如:

package com.example;

import io.cucumber.plugin.ConcurrentEventListener;
import io.cucumber.plugin.event.EventPublisher;
import io.cucumber.plugin.event.Result;
import io.cucumber.plugin.event.Status;
import io.cucumber.plugin.event.TestCase;
import io.cucumber.plugin.event.TestCaseFinished;

public class MyTestListener implements ConcurrentEventListener {
    @Override
    public void setEventPublisher(EventPublisher publisher) {
        publisher.registerHandlerFor(TestCaseFinished.class, this::handleTestCaseFinished);
    }

    private void handleTestCaseFinished(TestCaseFinished event) {
        TestCase testCase = event.getTestCase();
        Result result = event.getResult();
        Status status = result.getStatus();
        Throwable error = result.getError();
        String scenarioName = testCase.getName();
        String id = "" + testCase.getUri() + testCase.getLine();
        System.out.println("Testcase " + id + " - " + status.name());
    }
}

When using JUnit 4 and TestNG you can activate this plugin using:使用 JUnit 4 和 TestNG 时,您可以使用以下方法激活此插件:

@CucumberOptions(plugin="com.example.MyTestListener")

With JUnit 5 you add it to junit-platform.properties :使用 JUnit 5,您可以将其添加到junit-platform.properties

cucumber.plugin=com.example.MyTestListener 

Or if you are using the CLI或者,如果您使用的是 CLI

--plugin com.example.MyTestListener 

You can to this by writing your own custom implementation of Formatter & Reporter interface.您可以通过编写您自己的Formatter & Reporter接口的自定义实现来实现这一点。 The empty implementation of Formatter is the NullFormatter.java which you can extend. Formatter的空实现是您可以扩展的NullFormatter.java You will need to provide implementations for the Reporter interface.您将需要提供Reporter接口的实现。

The methods which would be of interest will be the result() of the Reporter interface and possibly the done() method of Formatter.感兴趣的方法将是 Reporter 接口的result()和 Formatter 的done()方法。 The result() has the Result object which has the exceptions. result() 具有包含异常的Result对象。

You can look at RerunFormatter.java for clarity.为清楚起见,您可以查看RerunFormatter.java

Github Formatter source Github 格式化程序源

public void result(Result result) {
      //Code to create logs or store to a database etc...
      result.getError();
      result.getErrorMessage();
}

You will need to add this class(com.myimpl.CustomFormRep) to the plugin option.您需要将此类(com.myimpl.CustomFormRep)添加到插件选项中。

plugin={"pretty", "html:report", "json:reports.json","rerun:target/rerun.txt",com.myimpl.CustomFormRep}

More details on custom formatters. 有关自定义格式化程序的更多详细信息。

You can use the rerun plugin to get a list of failed scenarios to run again.您可以使用重新运行插件获取失败场景的列表以再次运行。 Not sure about scheduling a run of failed tests, code to create a batch job or schedule one on your CI tool.不确定是否安排了一系列失败的测试、创建批处理作业的代码或在 CI 工具上安排一个。

This is the workaround for cucumber-java version 4.8.0 using reflection.这是使用反射的4.8.0 -java 版本4.8.0的解决方法。

import cucumber.api.Result;
import io.cucumber.core.api.Scenario;
import io.cucumber.core.logging.Logger;
import io.cucumber.core.logging.LoggerFactory;
import io.cucumber.java.After;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;

@After
public void afterScenario(Scenario scenario) throws IOException {
    if(!scenario.getStatus().isOk(true)){
        logError(scenario);
    }
}

private static void logError(Scenario scenario) {
    try {
        Class clasz = ClassUtils.getClass("cucumber.runtime.java.JavaHookDefinition$ScenarioAdaptor");
        Field fieldScenario = FieldUtils.getField(clasz, "scenario", true);
        fieldScenario.setAccessible(true);
        Object objectScenario =  fieldScenario.get(scenario);

        Field fieldStepResults = objectScenario.getClass().getDeclaredField("stepResults");
        fieldStepResults.setAccessible(true);

        ArrayList<Result> results = (ArrayList<Result>) fieldStepResults.get(objectScenario);
        for (Result result : results) {
            if (result.getError() != null) {
                LOGGER.error(String.format("Error Scenario: %s", scenario.getId()), result.getError());
            }
        }
    } catch (Exception e) {
        LOGGER.error("Error while logging error", e);
    }
}

If you just want to massage the result being sent to the report then you can extend the CucumberJSONFormatter and override the result method like this:如果您只想按摩发送到报告的结果,那么您可以扩展 CucumberJSONFormatter 并像这样覆盖 result 方法:

public class CustomReporter extends CucumberJSONFormatter {

    CustomReporter(Appendable out) {
        super(out);
    }

    /**
     * Truncate the error in the output to the testresult.json file.
     * @param result the error result
    */
    @Override
    void result(Result result) {
        String errorMessage = null;
        if (result.error) {
            errorMessage = "Error: " + truncateError(result.error);
        }
        Result myResult = new Result(result.status, result.duration, errorMessage);
        // Log the truncated error to the JSON report
        super.result(myResult);
    }
}

Then set the plugin option to:然后将插件选项设置为:

plugin = ["com.myimpl.CustomReporter:build/reports/json/testresult.json"]

For cucumber-js https://www.npmjs.com/package/cucumber/v/6.0.3对于cucumber-js https://www.npmjs.com/package/cucumber/v/6.0.3

import { After } from 'cucumber'

After(async function(scenario: any) {
    const exception = scenario.result.exception
    if (exception) {
        this.logger.log({ level: 'error', message: '-----------StackTrace-----------' })
        this.logger.log({ level: 'error', message: exception.stack })
        this.logger.log({ level: 'error', message: '-----------End-StackTrace-----------' })
    }
})

在此处输入图片说明

After a lot of experimentation I now removed the Before/After-Annotations and rely on Cucumber-Events instead.经过大量实验,我现在删除了 Before/After-Annotations 并依靠 Cucumber-Events 代替。 They contain the TestCase (which is what the Scenario -class wraps) and a Result where you can call getError();它们包含TestCase (这是Scenario类包装的内容)和一个Result ,您可以在其中调用getError(); to get the Throwable.得到 Throwable。

Here is a simple example to get it working这是一个让它工作的简单示例

import io.cucumber.plugin.EventListener;
import io.cucumber.plugin.event.EventPublisher;
import io.cucumber.plugin.event.Result;
import io.cucumber.plugin.event.Status;
import io.cucumber.plugin.event.TestCase;
import io.cucumber.plugin.event.TestCaseFinished;
import io.cucumber.plugin.event.TestCaseStarted;
import org.openqa.selenium.WebDriver;

public class TestCaseListener implements EventListener {
    @Override
    public void setEventPublisher(final EventPublisher publisher) {
        publisher.registerHandlerFor(TestCaseStarted.class, this::onTestCaseStarted);
        publisher.registerHandlerFor(TestCaseFinished.class, this::onTestCaseFinished);
    }

    public void onTestCaseStarted(TestCaseStarted event) {
        TestCase testCase = event.getTestCase();
        System.out.println("Starting " + testCase.getName());

        // Other stuff you did in your @Before-Method.
        // ...
    }

    private void onTestCaseFinished(final TestCaseFinished event) {
        TestCase testCase = event.getTestCase();
        System.out.println("Finished " + testCase.getName());

        Result result = event.getResult();
        if (result.getStatus() == Status.FAILED) {
            final Throwable error = result.getError();
            error.printStackTrace();
        }

        // Other stuff you did in your @After-Method.
        // ...
    }
}

All that's left to do is to register this class as a Cucumber-Plugin.剩下要做的就是将此类注册为 Cucumber-Plugin。 I did this by modifying my @CucumberOptions -annotation:我通过修改我的@CucumberOptions -annotation 来做到这一点:

@CucumberOptions(plugin = {"com.example.TestCaseListener"})

I find this much cleaner than all of this reflection-madness, however it requires a lot more code-changes.我发现这比所有这些疯狂的反射要干净得多,但是它需要更多的代码更改。

Edit编辑

I don't know why, but this caused a lot of tests to randomly fail in a multithreaded environment.我不知道为什么,但这导致很多测试在多线程环境中随机失败。 I tried to figure it out, but now also use the ugly reflections mentioned in this thread:我试图弄清楚,但现在也使用此线程中提到的丑陋反射:

public class SeleniumUtils {
private static final Logger log = LoggerFactory.getLogger(SeleniumUtils.class);

private static final Field field = FieldUtils.getField(Scenario.class, "delegate", true);
private static Method getError;

public static Throwable getError(Scenario scenario) {
    try {
        final TestCaseState testCase = (TestCaseState) field.get(scenario);
        if (getError == null) {
            getError = MethodUtils.getMatchingMethod(testCase.getClass(), "getError");
            getError.setAccessible(true);
        }
        return (Throwable) getError.invoke(testCase);
    } catch (Exception e) {
        log.warn("error receiving exception", e);
    }
    return null;
}
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM