【问题标题】:How to get the exception that was thrown when a Cucumber test failed in Java?如何获取 Java 中 Cucumber 测试失败时引发的异常?
【发布时间】:2017-03-01 21:38:03
【问题描述】:

我可以通过以下方式对测试失败执行操作:

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

但是,我需要执行的一些操作取决于引发的异常以及引发的上下文。有没有办法让 Throwable 导致测试失败?例如在 JUnit 中,我会通过扩展 TestWatcher 并在我的测试中添加一条规则来做到这一点:

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

但是 cucumber-junit 实现不允许使用规则,所以这个解决方案不适用于 Cucumber。

我认为我不需要解释为什么在测试失败时访问抛出的异常会有用,但是我仍然会提供一个示例:

我的测试环境并不总是稳定的,所以我的测试可能随时意外失败(没有特定的地方可以尝试捕获异常,因为它可能随时发生)。发生这种情况时,我需要重新安排测试以进行另一次尝试,并记录事件,以便我们可以获得有关环境不稳定性(何时、多频繁、多长时间等)的一些好的统计数据。

【问题讨论】:

    标签: java cucumber


    【解决方案1】:

    Frank Escobar 建议的变通问题:

    通过使用反射进入框架内部,您依赖于实现细节。这是一种不好的做法,当框架更改其实现时,您的代码可能会中断,正如您将在 Cucumber v5.0.0 中观察到的那样。

    Cucumber 中的 Hooks 旨在在场景之前和之后操纵测试执行上下文。他们不会报告测试执行本身。报告是跨领域的关注点,最好使用插件系统进行管理。

    例如:

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

    使用 JUnit 4 和 TestNG 时,您可以使用以下方式激活此插件:

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

    使用 JUnit 5,您可以将其添加到 junit-platform.properties:

    cucumber.plugin=com.example.MyTestListener 
    

    或者如果您使用的是 CLI

    --plugin com.example.MyTestListener 
    

    【讨论】:

      【解决方案2】:

      我已经使用反射实现了这个方法。您无法直接访问步骤错误(堆栈跟踪)。我创建了这个静态方法,它允许您访问“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);
         }
      }
      

      【讨论】:

      • 有没有办法让我在堆栈跟踪中得到这个result.getError()
      • @paul result.getError().getStackTrace() 这个? getError() 返回Throwable
      • 这种方法不再适用于 Cucumber 的更高版本,因为 ScenarioImpl 在 cucumber.runtime 中似乎不再可访问。有什么想法吗?
      • @Phonesis 具体来自哪个版本?
      • @FrancislainyCampos 谢谢,我也添加了。
      【解决方案3】:

      您可以通过编写自己的 Formatter &amp; Reporter 接口的自定义实现来实现这一点。 Formatter 的空实现是您可以扩展的 NullFormatter.java。您需要为Reporter 接口提供实现。

      感兴趣的方法是 Reporter 接口的 result() 和可能的 Formatter 的 done() 方法。 result() 的 Result 对象有异常。

      为了清楚起见,您可以查看RerunFormatter.java

      Github Formatter source

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

      您需要将此类 (com.myimpl.CustomFormRep) 添加到插件选项中。

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

      More details on custom formatters.

      您可以使用重新运行插件来获取失败场景的列表以再次运行。不确定如何安排运行失败的测试、创建批处理作业的代码或在 CI 工具上安排一个。

      【讨论】:

        【解决方案4】:

        这是 cucumber-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);
            }
        }
        

        【讨论】:

        • 上述函数不适用于 cucumber-java 5.2.0 版。它给出了 cucumber.runtime.java.JavaHookDefinition$ScenarioAdaptor class not found 的异常。
        • @Bilal 在描述中我明确指定的是 cucumber-java 版本 4.8.0。当库有从 4 到 5 的重大变化时,那是因为没有兼容性。也许版本 5 可以开箱即用地实现这一点。
        • 您能否指导 5.2.0 版,因为我是全新的,找不到任何帮助。
        【解决方案5】:

        对于cucumber-jshttps://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-----------' })
            }
        })
        

        【讨论】:

        • 这似乎不起作用,因为使用最新版本的黄瓜的场景对象中不存在异常。还有其他想法吗?也许有反思?
        • @Phonesis To 说你必须确定。 exception 属性是 Error 类型。
        • 我正在通过 npm 使用最新版本的黄瓜,但没有得到这个。我只看到持续时间和状态。
        • 只是一个更新,结果是“场景:任何”作为参数类型是这里的关键。它适用于任何类型的集合。如果我使用 HookScenarioResult 作为类型,它找不到异常。
        • @Phonesis 没错。使用HookScenarioResult,您正在将场景解析为该对象,并且该对象没有异常。
        【解决方案6】:

        经过大量实验后,我现在删除了之前/之后的注释并改用 Cucumber-Events。它们包含TestCase(这是Scenario-class 包装的内容)和Result,您可以在其中调用getError(); 来获取Throwable。

        这是一个让它工作的简单示例

        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.
                // ...
            }
        }
        

        剩下要做的就是将这个类注册为 Cucumber-Plugin。 我通过修改我的@CucumberOptions-annotation 来做到这一点:

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

        我发现这比所有这些疯狂的反射要干净得多,但是它需要更多的代码更改。

        编辑

        我不知道为什么,但这会导致很多测试在多线程环境中随机失败。 我试图弄清楚,但现在也使用这个线程中提到的丑陋反射:

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

        【讨论】:

        • 而不是使用EventListener,你应该使用ConcurrentEventListener。否则 Cucumber 只会在并发执行结束后为您提供事件。
        【解决方案7】:

        如果您只想处理发送到报告的结果,那么您可以扩展 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);
            }
        }
        

        然后将插件选项设置为:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-12-27
          • 2014-02-09
          • 1970-01-01
          • 2020-08-03
          • 1970-01-01
          • 1970-01-01
          • 2022-01-18
          • 2015-08-18
          相关资源
          最近更新 更多