Skip to content

Commit

Permalink
[Java] Add BeforeAll and AfterAll hooks (#1876)
Browse files Browse the repository at this point in the history
`BeforeAll` and `AfterAll` hooks are executed before all scenarios are executed and
after all scenarios have been executed. A hook is declared by annotating a method.
This methods must be static and do not take any arguments.

Hooks are global, all hooks declared in any step definition class will be 
executed. The order in which hooks are executed is not defined. An explicit
order can be provided by using the `order` property in the annotation.

```java
package io.cucumber.example;

import io.cucumber.java.AfterAll;
import io.cucumber.java.BeforeAll;

public class StepDefinitions {

    @BeforeAll
    public static void beforeAll() {
        // Runs before all scenarios
    }

    @afterall
    public static void afterAll() {
        // Runs after all scenarios
    }
}
```

Notes:

 1. When used in combination with Junit 5, Maven Surefire, and/or Failsafe use 
    version `3.0.0-M5` or later.
 2. When used in combination with Junit 5 and InteliJ IDEA failures in before
    all and after all hooks do not fail a test run.

Fixes: #515
  • Loading branch information
mpkorstanje authored Apr 3, 2021
1 parent 71bb1cc commit 3bc80b9
Show file tree
Hide file tree
Showing 53 changed files with 1,146 additions and 244 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] (In Git)

### Added
* [Java] Added `BeforeAll` and `AfterAll` hooks ([cucumber/#1876](https://github.com/cucumber/cucumber/pull/1876) M.P. Korstanje)

### Changed
* [Core] Updated Cucumber Expressions to v11 ([cucumber/#711](https://github.com/cucumber/cucumber/pull/771) M.P. Korstanje)
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/io/cucumber/core/backend/Glue.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
@API(status = API.Status.STABLE)
public interface Glue {

void addBeforeAllHook(StaticHookDefinition beforeAllHook);

void addAfterAllHook(StaticHookDefinition afterAllHook);

void addStepDefinition(StepDefinition stepDefinition);

void addBeforeHook(HookDefinition beforeHook);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.cucumber.core.backend;

import org.apiguardian.api.API;

@API(status = API.Status.EXPERIMENTAL)
public interface StaticHookDefinition extends Located {

void execute();

int getOrder();
}
4 changes: 0 additions & 4 deletions core/src/main/java/io/cucumber/core/cli/Main.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.cucumber.core.cli;

import io.cucumber.core.logging.Logger;
import io.cucumber.core.logging.LoggerFactory;
import io.cucumber.core.options.CommandlineOptionsParser;
import io.cucumber.core.options.Constants;
import io.cucumber.core.options.CucumberProperties;
Expand All @@ -28,8 +26,6 @@
@API(status = API.Status.STABLE)
public class Main {

private static final Logger log = LoggerFactory.getLogger(Main.class);

public static void main(String... argv) {
byte exitStatus = run(argv, Thread.currentThread().getContextClassLoader());
System.exit(exitStatus);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
package io.cucumber.core.exception;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public final class CompositeCucumberException extends CucumberException {

private final List<Throwable> causes;

public CompositeCucumberException(List<Throwable> causes) {
super(String.format("There were %d exceptions:", causes.size()));
this.causes = causes;
}

public List<Throwable> getCauses() {
return Collections.unmodifiableList(this.causes);
}

public String getMessage() {
return super.getMessage() + this.causes.stream()
.map(e -> String.format(" %s(%s)", e.getClass().getName(), e.getMessage()))
.collect(Collectors.joining("\n", "\n", ""));
super(String.format("There were %d exceptions. The details are in the stacktrace below.", causes.size()));
causes.forEach(this::addSuppressed);
}

}
10 changes: 5 additions & 5 deletions core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,9 @@ private Map<String, Object> createDummyFeatureForFailure(TestRunFinished event)

scenario.put("start_timestamp", getDateTimeFromTimeStamp(event.getInstant()));
scenario.put("line", 2);
scenario.put("name", "Could not execute Cucumber");
scenario.put("name", "Failure while executing Cucumber");
scenario.put("description", "");
scenario.put("id", "failure;could-not-execute-cucumber");
scenario.put("id", "failure;failure-while-executing-cucumber");
scenario.put("type", "scenario");
scenario.put("keyword", "Scenario");

Expand All @@ -372,18 +372,18 @@ private Map<String, Object> createDummyFeatureForFailure(TestRunFinished event)
whenResult.put("status", "passed");
}
when.put("line", 3);
when.put("name", "Cucumber could not execute");
when.put("name", "Cucumber failed while executing");
Map<String, Object> whenMatch = new LinkedHashMap<>();
when.put("match", whenMatch);
whenMatch.put("arguments", new ArrayList<>());
whenMatch.put("location", "io.cucumber.core.Failure.cucumber_could_not_execute()");
whenMatch.put("location", "io.cucumber.core.Failure.failure_while_executing_cucumber()");
when.put("keyword", "When ");

{
Map<String, Object> thenResult = new LinkedHashMap<>();
then.put("result", thenResult);
thenResult.put("duration", 0);
thenResult.put("error_message", exception.getMessage());
thenResult.put("error_message", printStackTrace(exception));
thenResult.put("status", "failed");
}
then.put("line", 4);
Expand Down
13 changes: 12 additions & 1 deletion core/src/main/java/io/cucumber/core/plugin/PrettyFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ private void handleEmbed(EmbedEvent event) {
}

private void handleTestRunFinished(TestRunFinished event) {
printError(event);
out.close();
}

Expand Down Expand Up @@ -132,11 +133,21 @@ private void printStep(TestStepFinished event) {

private void printError(TestStepFinished event) {
Result result = event.getResult();
printError(result);
}

private void printError(TestRunFinished event) {
Result result = event.getResult();
printError(result);
}

private void printError(Result result) {
Throwable error = result.getError();
if (error != null) {
String name = result.getStatus().name().toLowerCase(ROOT);
Format format = formats.get(name);
String text = printStackTrace(error);
out.println(" " + formats.get(name).text(text));
out.println(" " + format.text(text));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import io.cucumber.plugin.event.Status;
import io.cucumber.plugin.event.TestRunFinished;
import io.cucumber.plugin.event.TestStepFinished;
import io.cucumber.plugin.event.WriteEvent;

import java.io.OutputStream;
import java.util.HashMap;
Expand Down Expand Up @@ -51,7 +50,6 @@ public void setMonochrome(boolean monochrome) {
@Override
public void setEventPublisher(EventPublisher publisher) {
publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished);
publisher.registerHandlerFor(WriteEvent.class, this::handleWrite);
publisher.registerHandlerFor(TestRunFinished.class, event -> handleTestRunFinished());
}

Expand All @@ -67,10 +65,6 @@ private void handleTestStepFinished(TestStepFinished event) {
}
}

private void handleWrite(WriteEvent event) {
out.append(event.getText());
}

private void handleTestRunFinished() {
out.println();
out.close();
Expand Down
33 changes: 23 additions & 10 deletions core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@
import io.cucumber.plugin.event.TestStepStarted;
import io.cucumber.plugin.event.WriteEvent;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
Expand All @@ -45,6 +43,7 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static io.cucumber.core.exception.ExceptionUtils.printStackTrace;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.joining;

Expand Down Expand Up @@ -81,6 +80,13 @@ public class TeamCityPlugin implements EventListener {
private static final String TEMPLATE_TEST_IGNORED = TEAMCITY_PREFIX
+ "[testIgnored timestamp = '%s' duration = '%s' message = '%s' name = '%s']";

private static final String TEMPLATE_BEFORE_ALL_AFTER_ALL_STARTED = TEAMCITY_PREFIX
+ "[testStarted timestamp = '%s' name = '%s']";
private static final String TEMPLATE_BEFORE_ALL_AFTER_ALL_FAILED = TEAMCITY_PREFIX
+ "[testFailed timestamp = '%s' message = '%s' details = '%s' name = '%s']";
private static final String TEMPLATE_BEFORE_ALL_AFTER_ALL_FINISHED = TEAMCITY_PREFIX
+ "[testFinished timestamp = '%s' name = '%s']";

private static final String TEMPLATE_PROGRESS_COUNTING_STARTED = TEAMCITY_PREFIX
+ "[customProgressStatus testsCategory = 'Scenarios' count = '0' timestamp = '%s']";
private static final String TEMPLATE_PROGRESS_COUNTING_FINISHED = TEAMCITY_PREFIX
Expand Down Expand Up @@ -274,7 +280,7 @@ private void printTestStepFinished(TestStepFinished event) {
}
case AMBIGUOUS:
case FAILED: {
String details = extractStackTrace(error);
String details = printStackTrace(error);
print(TEMPLATE_TEST_FAILED, timeStamp, duration, "Step failed", details, name);
break;
}
Expand All @@ -284,13 +290,6 @@ private void printTestStepFinished(TestStepFinished event) {
print(TEMPLATE_TEST_FINISHED, timeStamp, duration, name);
}

private String extractStackTrace(Throwable error) {
ByteArrayOutputStream s = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(s);
error.printStackTrace(printStream);
return new String(s.toByteArray(), StandardCharsets.UTF_8);
}

private String extractName(TestStep step) {
if (step instanceof PickleStepTestStep) {
PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) step;
Expand Down Expand Up @@ -364,9 +363,23 @@ private void printTestRunFinished(TestRunFinished event) {
poppedNodes(emptyStack).forEach(node -> finishNode(timestamp, node));
currentStack = emptyStack;

printBeforeAfterAllResult(event, timestamp);
print(TEMPLATE_TEST_RUN_FINISHED, timestamp);
}

private void printBeforeAfterAllResult(TestRunFinished event, String timestamp) {
Throwable error = event.getResult().getError();
if (error == null) {
return;
}
// Use dummy test to display before all after all failures
String name = "Before All/After All";
print(TEMPLATE_BEFORE_ALL_AFTER_ALL_STARTED, timestamp, name);
String details = printStackTrace(error);
print(TEMPLATE_BEFORE_ALL_AFTER_ALL_FAILED, timestamp, "Before All/ After All failed", details, name);
print(TEMPLATE_BEFORE_ALL_AFTER_ALL_FINISHED, timestamp, name);
}

private void handleSnippetSuggested(SnippetsSuggestedEvent event) {
suggestions.add(event);
}
Expand Down
39 changes: 34 additions & 5 deletions core/src/main/java/io/cucumber/core/runner/CachingGlue.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.cucumber.core.backend.ParameterTypeDefinition;
import io.cucumber.core.backend.ScenarioScoped;
import io.cucumber.core.backend.StackTraceElementReference;
import io.cucumber.core.backend.StaticHookDefinition;
import io.cucumber.core.backend.StepDefinition;
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.gherkin.Step;
Expand Down Expand Up @@ -48,21 +49,27 @@

final class CachingGlue implements Glue {

private static final Comparator<CoreHookDefinition> ASCENDING = Comparator
private static final Comparator<CoreHookDefinition> HOOK_ORDER_ASCENDING = Comparator
.comparingInt(CoreHookDefinition::getOrder)
.thenComparing(ScenarioScoped.class::isInstance);

private static final Comparator<StaticHookDefinition> STATIC_HOOK_ORDER_ASCENDING = Comparator
.comparingInt(StaticHookDefinition::getOrder);

private final List<ParameterTypeDefinition> parameterTypeDefinitions = new ArrayList<>();
private final List<DataTableTypeDefinition> dataTableTypeDefinitions = new ArrayList<>();
private final List<DefaultParameterTransformerDefinition> defaultParameterTransformers = new ArrayList<>();
private final List<CoreDefaultDataTableEntryTransformerDefinition> defaultDataTableEntryTransformers = new ArrayList<>();
private final List<DefaultDataTableCellTransformerDefinition> defaultDataTableCellTransformers = new ArrayList<>();
private final List<DocStringTypeDefinition> docStringTypeDefinitions = new ArrayList<>();

private final List<StaticHookDefinition> beforeAllHooks = new ArrayList<>();
private final List<CoreHookDefinition> beforeHooks = new ArrayList<>();
private final List<CoreHookDefinition> beforeStepHooks = new ArrayList<>();
private final List<StepDefinition> stepDefinitions = new ArrayList<>();
private final List<CoreHookDefinition> afterStepHooks = new ArrayList<>();
private final List<CoreHookDefinition> afterHooks = new ArrayList<>();
private final List<StaticHookDefinition> afterAllHooks = new ArrayList<>();

/*
* Storing the pattern that matches the step text allows us to cache the
Expand All @@ -79,6 +86,18 @@ final class CachingGlue implements Glue {
this.bus = bus;
}

@Override
public void addBeforeAllHook(StaticHookDefinition beforeAllHook) {
beforeAllHooks.add(beforeAllHook);
beforeAllHooks.sort(STATIC_HOOK_ORDER_ASCENDING);
}

@Override
public void addAfterAllHook(StaticHookDefinition afterAllHook) {
afterAllHooks.add(afterAllHook);
afterAllHooks.sort(STATIC_HOOK_ORDER_ASCENDING);
}

@Override
public void addStepDefinition(StepDefinition stepDefinition) {
stepDefinitions.add(stepDefinition);
Expand All @@ -87,25 +106,25 @@ public void addStepDefinition(StepDefinition stepDefinition) {
@Override
public void addBeforeHook(HookDefinition hookDefinition) {
beforeHooks.add(CoreHookDefinition.create(hookDefinition));
beforeHooks.sort(ASCENDING);
beforeHooks.sort(HOOK_ORDER_ASCENDING);
}

@Override
public void addAfterHook(HookDefinition hookDefinition) {
afterHooks.add(CoreHookDefinition.create(hookDefinition));
afterHooks.sort(ASCENDING);
afterHooks.sort(HOOK_ORDER_ASCENDING);
}

@Override
public void addBeforeStepHook(HookDefinition hookDefinition) {
beforeStepHooks.add(CoreHookDefinition.create(hookDefinition));
beforeStepHooks.sort(ASCENDING);
beforeStepHooks.sort(HOOK_ORDER_ASCENDING);
}

@Override
public void addAfterStepHook(HookDefinition hookDefinition) {
afterStepHooks.add(CoreHookDefinition.create(hookDefinition));
afterStepHooks.sort(ASCENDING);
afterStepHooks.sort(HOOK_ORDER_ASCENDING);
}

@Override
Expand Down Expand Up @@ -143,6 +162,10 @@ public void addDocStringType(DocStringTypeDefinition docStringType) {
docStringTypeDefinitions.add(docStringType);
}

List<StaticHookDefinition> getBeforeAllHooks() {
return new ArrayList<>(beforeAllHooks);
}

Collection<CoreHookDefinition> getBeforeHooks() {
return new ArrayList<>(beforeHooks);
}
Expand All @@ -163,6 +186,12 @@ Collection<CoreHookDefinition> getAfterStepHooks() {
return hooks;
}

List<StaticHookDefinition> getAfterAllHooks() {
ArrayList<StaticHookDefinition> hooks = new ArrayList<>(afterAllHooks);
Collections.reverse(hooks);
return hooks;
}

Collection<ParameterTypeDefinition> getParameterTypeDefinitions() {
return parameterTypeDefinitions;
}
Expand Down
Loading

0 comments on commit 3bc80b9

Please sign in to comment.