diff --git a/deployment/src/main/java/io/quarkiverse/cucumber/deployment/CucumberProcessor.java b/deployment/src/main/java/io/quarkiverse/cucumber/CucumberProcessor.java similarity index 88% rename from deployment/src/main/java/io/quarkiverse/cucumber/deployment/CucumberProcessor.java rename to deployment/src/main/java/io/quarkiverse/cucumber/CucumberProcessor.java index 5dd968c..ddc1fcb 100644 --- a/deployment/src/main/java/io/quarkiverse/cucumber/deployment/CucumberProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/cucumber/CucumberProcessor.java @@ -1,4 +1,4 @@ -package io.quarkiverse.cucumber.deployment; +package io.quarkiverse.cucumber; import java.util.Arrays; import java.util.HashSet; @@ -8,7 +8,6 @@ import io.cucumber.java.StepDefinitionAnnotation; import io.cucumber.java.StepDefinitionAnnotations; -import io.quarkiverse.cucumber.CucumberQuarkusTest; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.annotations.BuildStep; @@ -16,8 +15,8 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +@SuppressWarnings("unused") class CucumberProcessor { - private static final String FEATURE = "cucumber"; @BuildStep @@ -53,6 +52,10 @@ AdditionalBeanBuildItem beanDefiningAnnotation(CombinedIndexBuildItem indexBuild .getAllKnownSubclasses(DotName.createSimple(CucumberQuarkusTest.class.getName()))) { stepClasses.add(i.name().toString()); } + for (var i : indexBuildItem.getIndex() + .getAllKnownSubclasses(DotName.createSimple(CucumberQuarkusIntegrationTest.class.getName()))) { + stepClasses.add(i.name().toString()); + } return AdditionalBeanBuildItem.builder().addBeanClasses(stepClasses).setDefaultScope(DotNames.SINGLETON) .setUnremovable().build(); } diff --git a/deployment/src/test/java/io/quarkiverse/cucumber/test/BasicTest.java b/deployment/src/test/java/io/quarkiverse/cucumber/test/BasicTest.java index 01e795c..f903b32 100644 --- a/deployment/src/test/java/io/quarkiverse/cucumber/test/BasicTest.java +++ b/deployment/src/test/java/io/quarkiverse/cucumber/test/BasicTest.java @@ -3,5 +3,4 @@ import io.quarkiverse.cucumber.CucumberQuarkusTest; public class BasicTest extends CucumberQuarkusTest { - -} +} \ No newline at end of file diff --git a/deployment/src/test/java/io/quarkiverse/cucumber/test/Endpoint.java b/deployment/src/test/java/io/quarkiverse/cucumber/test/Endpoint.java index 38b7fbe..bbeddad 100644 --- a/deployment/src/test/java/io/quarkiverse/cucumber/test/Endpoint.java +++ b/deployment/src/test/java/io/quarkiverse/cucumber/test/Endpoint.java @@ -5,9 +5,8 @@ @Path("/") public class Endpoint { - @GET public String hello() { return "hello"; } -} +} \ No newline at end of file diff --git a/deployment/src/test/java/io/quarkiverse/cucumber/test/Steps.java b/deployment/src/test/java/io/quarkiverse/cucumber/test/Steps.java index 06c114a..9fe41f7 100644 --- a/deployment/src/test/java/io/quarkiverse/cucumber/test/Steps.java +++ b/deployment/src/test/java/io/quarkiverse/cucumber/test/Steps.java @@ -11,23 +11,24 @@ import io.restassured.response.ValidatableResponse; public class Steps { + private final String target; @Inject - @ConfigProperty(defaultValue = "/") - String target; + public Steps(@ConfigProperty(name = "some.endpoint.path", defaultValue = "/") String target) { + this.target = target; + } private ValidatableResponse result; @Given("I call the endpoint") - public void i_call_endpoint() throws Exception { + public void i_call_endpoint() { result = given() .when().get(target) .then(); } @Then("the response is ok") - public void response_is_ok() throws Exception { + public void response_is_ok() { result.statusCode(200); } - -} +} \ No newline at end of file diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index cbbe775..5b8be14 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -21,6 +21,8 @@ In your `pom.xml` file, add: == Usage +=== Quarkus Test setup + To bootstrap Cucumber add the following class to your test suite: [source,java] @@ -28,12 +30,45 @@ To bootstrap Cucumber add the following class to your test suite: import io.quarkiverse.cucumber.CucumberQuarkusTest; public class MyTest extends CucumberQuarkusTest { +} +---- + +This will automatically bootstrap Cucumber, and discover any `.feature` files and step classes that provide glue code. + +=== Integration test setup + +For integration tests to work, the `weld-junit5` extension is used and preconfigured. Aside from enabling integration test support, it also enabled basic injection support. + +To run a cucumber test as integration test just extend `CucumberQuarkusIntegrationTest`: + +[source,java] +---- +import io.quarkiverse.cucumber.CucumberQuarkusIntegrationTest; +public class MyIT extends CucumberQuarkusIntegrationTest { } +---- + +In case the integration test is located in a sibling-package of other bean-defining classes, we have to configure the packages to scan: +[source,java] ---- +import io.quarkiverse.cucumber.CucumberIntegrationTest; -This will automatically bootstrap Cucumber, and discover any `.feature` files and step classes that provide glue code. +public class MyIT extends CucumberQuarkusIntegrationTest { + @Override + protected Class[] packagesToScanRecursively() { + return new Class[] { SomeClassInASiblingPackage.class, SomeClassInAnotherSiblingPackage.class }; + } +} +---- + +Weld will scan these packages recursively. This means that if a class is located in a package that is a parent of all other bean-defining packages, it is sufficient to reference this bean (see `CucumberOptionsIT` for an example). + +As of now, the injection support has the following limitations: + +* Only beans with `@ApplicationScoped` are detected +* classes defining the glue need to be annotated with `@ApplicationScoped` == IDE Integration @@ -50,7 +85,7 @@ You need to add the following `main` method to your test class: import io.quarkiverse.cucumber.CucumberQuarkusTest; public class MyTest extends CucumberQuarkusTest { - public static void main(String[] args) { + public static void main(String... args) { runMain(MyTest.class, args); } } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 1aa5d19..af61012 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 io.quarkiverse.cucumber @@ -42,6 +43,10 @@ + + maven-failsafe-plugin + ${version.surefire.plugin} + @@ -60,24 +65,6 @@ ${native.surefire.skip} - - maven-failsafe-plugin - - - - integration-test - verify - - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - ${maven.home} - - - - - diff --git a/integration-tests/src/main/java/io/quarkiverse/cucumber/it/CucumberResource.java b/integration-tests/src/main/java/io/quarkiverse/cucumber/it/CucumberResource.java index f01f472..e4fc4d1 100644 --- a/integration-tests/src/main/java/io/quarkiverse/cucumber/it/CucumberResource.java +++ b/integration-tests/src/main/java/io/quarkiverse/cucumber/it/CucumberResource.java @@ -1,31 +1,25 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ package io.quarkiverse.cucumber.it; -import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; -@Path("/cucumber") -@ApplicationScoped +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@Path(CucumberResource.PATH) +@Produces(MediaType.TEXT_PLAIN) public class CucumberResource { + public static final String PATH = "cucumber"; + + private final String greeting; + + public CucumberResource(@ConfigProperty(name = "greeting") String greeting) { + this.greeting = greeting; + } @GET public String hello() { - return "Hello cucumber"; + return greeting; } } diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index 6942959..d25825f 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -1 +1 @@ -testPath=/cucumber \ No newline at end of file +greeting="Hello, cucumber!" \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/CucumberResourceIT.java b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/CucumberResourceIT.java new file mode 100644 index 0000000..230aced --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/CucumberResourceIT.java @@ -0,0 +1,9 @@ +package io.quarkiverse.cucumber.it; + +import io.quarkiverse.cucumber.CucumberQuarkusIntegrationTest; + +public class CucumberResourceIT extends CucumberQuarkusIntegrationTest { + public static void main(String... args) { + runMain(CucumberResourceIT.class, args); + } +} \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/CucumberResourceTest.java b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/CucumberResourceTest.java index 6a52cfa..2bbed05 100644 --- a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/CucumberResourceTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/CucumberResourceTest.java @@ -3,7 +3,7 @@ import io.quarkiverse.cucumber.CucumberQuarkusTest; public class CucumberResourceTest extends CucumberQuarkusTest { - public static void main(String[] args) { + public static void main(String... args) { runMain(CucumberResourceTest.class, args); } } diff --git a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/Steps.java b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/Steps.java deleted file mode 100644 index 95c44ad..0000000 --- a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/Steps.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.quarkiverse.cucumber.it; - -import static io.restassured.RestAssured.given; - -import jakarta.inject.Inject; - -import org.eclipse.microprofile.config.inject.ConfigProperty; - -import io.cucumber.java.en.Given; -import io.cucumber.java.en.Then; -import io.restassured.response.ValidatableResponse; - -public class Steps { - - @Inject - @ConfigProperty(name = "testPath", defaultValue = "/") - String target; - - private ValidatableResponse result; - - @Given("^print \"(.+)\"$") - public void print(String message) throws Exception { - System.out.println(message); - } - - @Given("I call the endpoint") - public void i_call_endpoint() throws Exception { - result = given() - .when().get(target) - .then(); - } - - @Then("the response is ok") - public void response_is_ok() throws Exception { - result.statusCode(200); - } - -} diff --git a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/actors/CucumberResourceActor.java b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/actors/CucumberResourceActor.java new file mode 100644 index 0000000..eb4f2dd --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/actors/CucumberResourceActor.java @@ -0,0 +1,44 @@ +package io.quarkiverse.cucumber.it.actors; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.is; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import io.quarkiverse.cucumber.it.CucumberResource; +import io.restassured.response.ValidatableResponse; + +@ApplicationScoped +public class CucumberResourceActor { + private final String greeting; + + @Inject + public CucumberResourceActor(@ConfigProperty(name = "greeting") String greeting) { + this.greeting = greeting; + } + + private ValidatableResponse response; + + public void callTarget() { + response = when().get(CucumberResource.PATH) + .then(); + } + + public void verifyResponse(int code) { + try { + response.statusCode(code) + .contentType(MediaType.TEXT_PLAIN) + .body(is(greeting)); + } finally { + resetState(); + } + } + + private void resetState() { + response = null; + } +} diff --git a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/options/CucumberOptionsIT.java b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/options/CucumberOptionsIT.java new file mode 100644 index 0000000..d0561d3 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/options/CucumberOptionsIT.java @@ -0,0 +1,20 @@ +package io.quarkiverse.cucumber.it.options; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.cucumber.CucumberOptions; +import io.quarkiverse.cucumber.CucumberQuarkusIntegrationTest; +import io.quarkiverse.cucumber.it.CucumberResourceIT; + +@CucumberOptions(glue = { "io.quarkiverse.cucumber.it.steps" }, tags = "@important", plugin = { "json" }) +@ApplicationScoped +public class CucumberOptionsIT extends CucumberQuarkusIntegrationTest { + public static void main(String... args) { + runMain(CucumberOptionsIT.class, args); + } + + @Override + protected Package[] packagesToScanRecursively() { + return new Package[] { CucumberResourceIT.class.getPackage() }; + } +} \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/options/CucumberOptionsTest.java b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/options/CucumberOptionsTest.java index a177a39..80eedd1 100644 --- a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/options/CucumberOptionsTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/options/CucumberOptionsTest.java @@ -3,9 +3,9 @@ import io.quarkiverse.cucumber.CucumberOptions; import io.quarkiverse.cucumber.CucumberQuarkusTest; -@CucumberOptions(glue = { "io.quarkiverse.cucumber.it" }, tags = "@important", plugin = { "json" }) +@CucumberOptions(glue = { "io.quarkiverse.cucumber.it.steps" }, tags = "@important", plugin = { "json" }) public class CucumberOptionsTest extends CucumberQuarkusTest { - public static void main(String[] args) { + public static void main(String... args) { runMain(CucumberOptionsTest.class, args); } } diff --git a/integration-tests/src/test/java/io/quarkiverse/cucumber/it/steps/CucumberResourceSteps.java b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/steps/CucumberResourceSteps.java new file mode 100644 index 0000000..31e3d90 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/cucumber/it/steps/CucumberResourceSteps.java @@ -0,0 +1,34 @@ +package io.quarkiverse.cucumber.it.steps; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; + +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.quarkiverse.cucumber.it.actors.CucumberResourceActor; + +@ApplicationScoped +public class CucumberResourceSteps { + private final CucumberResourceActor actor; + + @Inject + public CucumberResourceSteps(CucumberResourceActor actor) { + this.actor = actor; + } + + @Given("^print \"(.+)\"$") + public void print(String message) { + System.out.println(message); + } + + @Given("I call the endpoint") + public void i_call_endpoint() { + actor.callTarget(); + } + + @Then("the response is ok") + public void response_is_ok() { + actor.verifyResponse(Response.Status.OK.getStatusCode()); + } +} diff --git a/pom.xml b/pom.xml index 782129f..bd7b8be 100644 --- a/pom.xml +++ b/pom.xml @@ -23,13 +23,17 @@ 3.8.1 + true 11 11 UTF-8 UTF-8 + false + 3.0.3.Final 7.11.2 + 4.0.0.Final @@ -45,6 +49,11 @@ cucumber-java ${cucumber.version} + + org.jboss.weld + weld-junit5 + ${weld.version} + @@ -59,6 +68,26 @@ maven-compiler-plugin ${compiler-plugin.version} + + maven-failsafe-plugin + ${version.surefire.plugin} + + + + integration-test + verify + + + skipITs + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + diff --git a/runtime/pom.xml b/runtime/pom.xml index 87be420..53b9837 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -29,6 +29,10 @@ io.quarkus quarkus-junit5 + + org.jboss.weld + weld-junit5 + diff --git a/runtime/src/main/java/io/quarkiverse/cucumber/CucumberBaseTest.java b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberBaseTest.java new file mode 100644 index 0000000..15fdaca --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberBaseTest.java @@ -0,0 +1,244 @@ +package io.quarkiverse.cucumber; + +import java.net.URI; +import java.time.Clock; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.CDI; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.platform.console.ConsoleLauncher; + +import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.EventBus; +import io.cucumber.core.feature.FeatureParser; +import io.cucumber.core.filter.Filters; +import io.cucumber.core.gherkin.Pickle; +import io.cucumber.core.options.CommandlineOptionsParser; +import io.cucumber.core.options.Constants; +import io.cucumber.core.options.CucumberOptionsAnnotationParser; +import io.cucumber.core.options.CucumberProperties; +import io.cucumber.core.options.CucumberPropertiesParser; +import io.cucumber.core.options.RuntimeOptions; +import io.cucumber.core.options.RuntimeOptionsBuilder; +import io.cucumber.core.plugin.Options; +import io.cucumber.core.plugin.PluginFactory; +import io.cucumber.core.plugin.Plugins; +import io.cucumber.core.plugin.PrettyFormatter; +import io.cucumber.core.runner.Runner; +import io.cucumber.core.runtime.CucumberExecutionContext; +import io.cucumber.core.runtime.ExitStatus; +import io.cucumber.core.runtime.FeaturePathFeatureSupplier; +import io.cucumber.core.runtime.FeatureSupplier; +import io.cucumber.core.runtime.ObjectFactorySupplier; +import io.cucumber.core.runtime.TimeServiceEventBus; +import io.cucumber.java.JavaBackendProviderService; +import io.cucumber.plugin.event.EventHandler; +import io.cucumber.plugin.event.PickleStepTestStep; +import io.cucumber.plugin.event.Status; +import io.cucumber.plugin.event.TestStep; +import io.cucumber.plugin.event.TestStepFinished; + +abstract class CucumberBaseTest { + @TestFactory + List getTests() { + EventBus eventBus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); + final FeatureParser parser = new FeatureParser(eventBus::generateId); + + RuntimeOptions propertiesFileOptions = new CucumberPropertiesParser() + .parse(CucumberProperties.fromPropertiesFile()) + .build(); + + RuntimeOptions environmentOptions = new CucumberPropertiesParser() + .parse(CucumberProperties.fromEnvironment()) + .build(propertiesFileOptions); + + RuntimeOptions systemOptions = new CucumberPropertiesParser() + .parse(CucumberProperties.fromSystemProperties()) + .build(environmentOptions); + + RuntimeOptions runtimeOptions; + RuntimeOptionsBuilder runtimeOptionsBuilder = new RuntimeOptionsBuilder() + .addDefaultFeaturePathIfAbsent() + .addDefaultGlueIfAbsent() + .addDefaultSummaryPrinterIfNotDisabled(); + + QuarkusCucumberOptionsProvider optionsProvider = new QuarkusCucumberOptionsProvider(); + if (optionsProvider.hasOptions(this.getClass())) { + CucumberOptionsAnnotationParser annotationParser = new CucumberOptionsAnnotationParser() + .withOptionsProvider(optionsProvider); + RuntimeOptions annotationOptions = annotationParser + .parse(this.getClass()) + .build(systemOptions); + runtimeOptions = runtimeOptionsBuilder.build(annotationOptions); + } else { + runtimeOptions = runtimeOptionsBuilder.build(systemOptions); + } + + FeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(() -> Thread.currentThread().getContextClassLoader(), + runtimeOptions, parser); + + final Plugins plugins = new Plugins(new PluginFactory(), runtimeOptions); + plugins.addPlugin(new PrettyFormatter(System.out)); + + final ExitStatus exitStatus = new ExitStatus(runtimeOptions); + plugins.addPlugin(exitStatus); + if (runtimeOptions.isMultiThreaded()) { + plugins.setSerialEventBusOnEventListenerPlugins(eventBus); + } else { + plugins.setEventBusOnEventListenerPlugins(eventBus); + } + ObjectFactory objectFactory = new CdiObjectFactory(); + + ObjectFactorySupplier objectFactorySupplier = () -> objectFactory; + + Runner runner = new Runner(eventBus, + Collections.singleton(new JavaBackendProviderService().create(objectFactorySupplier.get(), + objectFactorySupplier.get(), + () -> Thread.currentThread() + .getContextClassLoader())), + objectFactorySupplier.get(), + runtimeOptions); + + CucumberExecutionContext context = new CucumberExecutionContext(eventBus, exitStatus, () -> runner); + + List features = new LinkedList<>(); + features.add(DynamicTest.dynamicTest("Start Cucumber", context::startTestRun)); + + Predicate filters = new Filters(runtimeOptions); + + featureSupplier.get().forEach(f -> { + List tests = new LinkedList<>(); + tests.add(DynamicTest.dynamicTest("Start Feature", () -> context.beforeFeature(f))); + f.getPickles() + .stream() + .filter(filters) + .forEach(p -> tests.add(DynamicTest.dynamicTest(p.getName(), () -> { + AtomicReference resultAtomicReference = new AtomicReference<>(); + EventHandler handler = event -> { + if (event.getResult().getStatus() != Status.PASSED) { + // save the first failed test step, so that we can get the line number of the cucumber file + resultAtomicReference.compareAndSet(null, event); + } + }; + eventBus.registerHandlerFor(TestStepFinished.class, handler); + context.runTestCase(r -> r.runPickle(p)); + eventBus.removeHandlerFor(TestStepFinished.class, handler); + + // if we have no main arguments, we are running as part of a junit test suite, we need to fail the junit test explicitly + if (resultAtomicReference.get() != null) { + TestStep testStep = resultAtomicReference.get().getTestStep(); + if (testStep instanceof PickleStepTestStep) { + // failed in step, we have a line in the feature file + Assertions.fail( + "failed in " + f.getUri() + " at line " + + ((PickleStepTestStep) testStep).getStep() + .getLocation() + .getLine(), + resultAtomicReference.get().getResult().getError()); + } else { + // failed somewhere in hooks + Assertions.fail( + "failed in " + f.getUri() + " at " + + testStep.getCodeLocation(), + resultAtomicReference.get().getResult().getError()); + } + } + }))); + + if (tests.size() > 1) { + features.add(DynamicContainer.dynamicContainer(f.getName().orElse(f.getSource()), tests.stream())); + } + }); + + features.add(DynamicTest.dynamicTest("Finish Cucumber", context::finishTestRun)); + + return features; + } + + public static class CdiObjectFactory implements ObjectFactory { + public void start() { + } + + public void stop() { + } + + public boolean addClass(Class clazz) { + return true; + } + + public T getInstance(Class type) { + var old = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(type.getClassLoader()); + Instance selected = CDI.current().select(type); + if (selected.isUnsatisfied()) { + throw new IllegalArgumentException(type.getName() + " is no CDI bean."); + } else { + return selected.get(); + } + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + } + + protected static void runMain(Class testClass, String... args) { + RuntimeOptions propertiesFileOptions = new CucumberPropertiesParser() + .parse(CucumberProperties.fromPropertiesFile()) + .build(); + + RuntimeOptions environmentOptions = new CucumberPropertiesParser() + .parse(CucumberProperties.fromEnvironment()) + .build(propertiesFileOptions); + + RuntimeOptions systemOptions = new CucumberPropertiesParser() + .parse(CucumberProperties.fromSystemProperties()) + .build(environmentOptions); + + CommandlineOptionsParser commandlineOptionsParser = new CommandlineOptionsParser(System.out); + RuntimeOptions runtimeOptions = commandlineOptionsParser.parse(args).build(systemOptions); + + commandlineOptionsParser.exitStatus().ifPresent(System::exit); + + System.setProperty(Constants.ANSI_COLORS_DISABLED_PROPERTY_NAME, String.valueOf(runtimeOptions.isMonochrome())); + //TODO: CUCUMBER_PROPERTIES_FILE_NAME + System.setProperty(Constants.EXECUTION_DRY_RUN_PROPERTY_NAME, String.valueOf(runtimeOptions.isDryRun())); + System.setProperty(Constants.EXECUTION_LIMIT_PROPERTY_NAME, String.valueOf(runtimeOptions.getLimitCount())); + //TODO: EXECUTION_ORDER_PROPERTY_NAME runtimeOptions.getPickleOrder(); (how can we convert this?) + //--strict/--no-strict is already handled by the CommandlineOptionsParser EXECUTION_STRICT_PROPERTY_NAME + System.setProperty(Constants.WIP_PROPERTY_NAME, String.valueOf(runtimeOptions.isWip())); + System.setProperty(Constants.FEATURES_PROPERTY_NAME, + runtimeOptions.getFeaturePaths().stream().map(URI::toString).collect(Collectors.joining(","))); + System.setProperty(Constants.FILTER_NAME_PROPERTY_NAME, + runtimeOptions.getNameFilters().stream().map(Pattern::toString).collect(Collectors.joining(","))); + System.setProperty(Constants.FILTER_TAGS_PROPERTY_NAME, + runtimeOptions.getTagExpressions().stream().map(Object::toString).collect(Collectors.joining(","))); + System.setProperty(Constants.GLUE_PROPERTY_NAME, + runtimeOptions.getGlue().stream().map(URI::toString).collect(Collectors.joining(","))); + Optional.ofNullable(runtimeOptions.getObjectFactoryClass()) + .ifPresent(s -> System.setProperty(Constants.OBJECT_FACTORY_PROPERTY_NAME, s.getName())); + System.setProperty(Constants.PLUGIN_PROPERTY_NAME, + runtimeOptions.plugins().stream().map(Options.Plugin::pluginString).collect(Collectors.joining(","))); + //Not supported via CLI argument: PLUGIN_PUBLISH_ENABLED_PROPERTY_NAME + //Not supported via CLI argument: PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME + //Not supported via CLI argument: PLUGIN_PUBLISH_URL_PROPERTY_NAME + //Not supported via CLI argument: PLUGIN_PUBLISH_QUIET_PROPERTY_NAME + System.setProperty(Constants.SNIPPET_TYPE_PROPERTY_NAME, runtimeOptions.getSnippetType().toString().toLowerCase()); + + ConsoleLauncher.main("-c", testClass.getName()); + } +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/cucumber/CucumberOptions.java b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberOptions.java index 10e3ed2..d606c4f 100644 --- a/runtime/src/main/java/io/quarkiverse/cucumber/CucumberOptions.java +++ b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberOptions.java @@ -19,9 +19,8 @@ @Target({ ElementType.TYPE }) @API(status = API.Status.STABLE) public @interface CucumberOptions { - /** - * @return true if glue code execution should be skipped. + * @return true if glue code execution should be skipped */ boolean dryRun() default false; @@ -34,7 +33,7 @@ * {@code com.example.RunCucumber} then features are assumed to be located * in {@code classpath:com/example}. * - * @return list of files or directories + * @return array of files or directories * @see io.cucumber.core.feature.FeatureWithLines */ String[] features() default {}; @@ -48,7 +47,7 @@ * {@code com.example.RunCucumber} then glue is assumed to be located in * {@code com.example}. * - * @return list of package names + * @return array of package names * @see io.cucumber.core.feature.GluePath */ String[] glue() default {}; @@ -60,7 +59,7 @@ * These packages are used in addition to the default described in * {@code #glue}. * - * @return list of package names + * @return array of package names */ String[] extraGlue() default {}; @@ -84,21 +83,21 @@ * Plugins can be provided with an argument. For example * {@code json:target/cucumber-report.json} * - * @return list of plugins + * @return array of plugins * @see Plugin */ String[] plugin() default {}; /** - * Publish report to https://reports.cucumber.io. + * Publish report to https://reports.cucumber.io. *

* - * @return true if reports should be published on the web. + * @return {@code true} if reports should be published on the web */ boolean publish() default false; /** - * @return true if terminal output should be without colours. + * @return {@code true} if terminal output should be without colours */ boolean monochrome() default false; @@ -106,12 +105,12 @@ * Only run scenarios whose names match one of the provided regular * expressions. * - * @return a list of regular expressions + * @return an array of regular expressions */ String[] name() default {}; /** - * @return the format of the generated snippets. + * @return the format of the generated snippets */ SnippetType snippets() default SnippetType.UNDERSCORE; @@ -124,6 +123,5 @@ * * @return an {@link io.cucumber.core.backend.ObjectFactory} implementation */ - Class objectFactory() default CucumberQuarkusTest.CdiObjectFactory.class; - -} + Class objectFactory() default CucumberBaseTest.CdiObjectFactory.class; +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/cucumber/CucumberQuarkusIntegrationTest.java b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberQuarkusIntegrationTest.java new file mode 100644 index 0000000..814202b --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberQuarkusIntegrationTest.java @@ -0,0 +1,24 @@ +package io.quarkiverse.cucumber; + +import org.jboss.weld.junit5.EnableWeld; +import org.jboss.weld.junit5.WeldInitiator; +import org.jboss.weld.junit5.WeldSetup; + +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.smallrye.config.inject.ConfigExtension; + +@QuarkusIntegrationTest +@EnableWeld +public abstract class CucumberQuarkusIntegrationTest extends CucumberBaseTest { + @WeldSetup + @SuppressWarnings("unused") + WeldInitiator weld = WeldInitiator.from( + WeldInitiator.createWeld() + .addExtensions(new ConfigExtension()) + .addPackages(true, packagesToScanRecursively())) + .build(); + + protected Package[] packagesToScanRecursively() { + return new Package[] { this.getClass().getPackage() }; + } +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/cucumber/CucumberQuarkusTest.java b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberQuarkusTest.java index 1252e43..bb90e0a 100644 --- a/runtime/src/main/java/io/quarkiverse/cucumber/CucumberQuarkusTest.java +++ b/runtime/src/main/java/io/quarkiverse/cucumber/CucumberQuarkusTest.java @@ -1,252 +1,7 @@ package io.quarkiverse.cucumber; -import java.net.URI; -import java.time.Clock; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import jakarta.enterprise.inject.Instance; -import jakarta.enterprise.inject.spi.CDI; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DynamicContainer; -import org.junit.jupiter.api.DynamicNode; -import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.TestFactory; -import org.junit.platform.console.ConsoleLauncher; - -import io.cucumber.core.backend.ObjectFactory; -import io.cucumber.core.eventbus.EventBus; -import io.cucumber.core.feature.FeatureParser; -import io.cucumber.core.filter.Filters; -import io.cucumber.core.gherkin.Pickle; -import io.cucumber.core.options.CommandlineOptionsParser; -import io.cucumber.core.options.Constants; -import io.cucumber.core.options.CucumberOptionsAnnotationParser; -import io.cucumber.core.options.CucumberProperties; -import io.cucumber.core.options.CucumberPropertiesParser; -import io.cucumber.core.options.RuntimeOptions; -import io.cucumber.core.options.RuntimeOptionsBuilder; -import io.cucumber.core.plugin.Options; -import io.cucumber.core.plugin.PluginFactory; -import io.cucumber.core.plugin.Plugins; -import io.cucumber.core.plugin.PrettyFormatter; -import io.cucumber.core.runner.Runner; -import io.cucumber.core.runtime.CucumberExecutionContext; -import io.cucumber.core.runtime.ExitStatus; -import io.cucumber.core.runtime.FeaturePathFeatureSupplier; -import io.cucumber.core.runtime.FeatureSupplier; -import io.cucumber.core.runtime.ObjectFactorySupplier; -import io.cucumber.core.runtime.TimeServiceEventBus; -import io.cucumber.java.JavaBackendProviderService; -import io.cucumber.plugin.event.EventHandler; -import io.cucumber.plugin.event.PickleStepTestStep; -import io.cucumber.plugin.event.Status; -import io.cucumber.plugin.event.TestStep; -import io.cucumber.plugin.event.TestStepFinished; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest -public abstract class CucumberQuarkusTest { - - @TestFactory - List getTests() { - EventBus eventBus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); - final FeatureParser parser = new FeatureParser(eventBus::generateId); - - RuntimeOptions propertiesFileOptions = new CucumberPropertiesParser() - .parse(CucumberProperties.fromPropertiesFile()) - .build(); - - RuntimeOptions environmentOptions = new CucumberPropertiesParser() - .parse(CucumberProperties.fromEnvironment()) - .build(propertiesFileOptions); - - RuntimeOptions systemOptions = new CucumberPropertiesParser() - .parse(CucumberProperties.fromSystemProperties()) - .build(environmentOptions); - - RuntimeOptions runtimeOptions; - RuntimeOptionsBuilder runtimeOptionsBuilder = new RuntimeOptionsBuilder() - .addDefaultFeaturePathIfAbsent() - .addDefaultGlueIfAbsent() - .addDefaultSummaryPrinterIfNotDisabled(); - - QuarkusCucumberOptionsProvider optionsProvider = new QuarkusCucumberOptionsProvider(); - if (optionsProvider.hasOptions(this.getClass())) { - CucumberOptionsAnnotationParser annotationParser = new CucumberOptionsAnnotationParser() - .withOptionsProvider(optionsProvider); - RuntimeOptions annotationOptions = annotationParser - .parse(this.getClass()) - .build(systemOptions); - runtimeOptions = runtimeOptionsBuilder.build(annotationOptions); - } else { - runtimeOptions = runtimeOptionsBuilder.build(systemOptions); - } - - FeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(() -> Thread.currentThread().getContextClassLoader(), - runtimeOptions, parser); - - final Plugins plugins = new Plugins(new PluginFactory(), runtimeOptions); - plugins.addPlugin(new PrettyFormatter(System.out)); - - final ExitStatus exitStatus = new ExitStatus(runtimeOptions); - plugins.addPlugin(exitStatus); - if (runtimeOptions.isMultiThreaded()) { - plugins.setSerialEventBusOnEventListenerPlugins(eventBus); - } else { - plugins.setEventBusOnEventListenerPlugins(eventBus); - } - ObjectFactory objectFactory = new CdiObjectFactory(); - - ObjectFactorySupplier objectFactorySupplier = () -> objectFactory; - - Runner runner = new Runner(eventBus, - Collections.singleton(new JavaBackendProviderService().create(objectFactorySupplier.get(), - objectFactorySupplier.get(), - () -> Thread.currentThread() - .getContextClassLoader())), - objectFactorySupplier.get(), - runtimeOptions); - - CucumberExecutionContext context = new CucumberExecutionContext(eventBus, exitStatus, () -> runner); - - List features = new LinkedList<>(); - features.add(DynamicTest.dynamicTest("Start Cucumber", context::startTestRun)); - - Predicate filters = new Filters(runtimeOptions); - - featureSupplier.get().forEach(f -> { - List tests = new LinkedList<>(); - tests.add(DynamicTest.dynamicTest("Start Feature", () -> context.beforeFeature(f))); - f.getPickles() - .stream() - .filter(filters) - .forEach(p -> tests.add(DynamicTest.dynamicTest(p.getName(), () -> { - AtomicReference resultAtomicReference = new AtomicReference<>(); - EventHandler handler = event -> { - if (event.getResult().getStatus() != Status.PASSED) { - // save the first failed test step, so that we can get the line number of the cucumber file - resultAtomicReference.compareAndSet(null, event); - } - }; - eventBus.registerHandlerFor(TestStepFinished.class, handler); - context.runTestCase(r -> r.runPickle(p)); - eventBus.removeHandlerFor(TestStepFinished.class, handler); - - // if we have no main arguments, we are running as part of a junit test suite, we need to fail the junit test explicitly - if (resultAtomicReference.get() != null) { - TestStep testStep = resultAtomicReference.get().getTestStep(); - if (testStep instanceof PickleStepTestStep) { - // failed in step, we have a line in the feature file - Assertions.fail( - "failed in " + f.getUri() + " at line " - + ((PickleStepTestStep) testStep).getStep() - .getLocation() - .getLine(), - resultAtomicReference.get().getResult().getError()); - } else { - // failed somewhere in hooks - Assertions.fail( - "failed in " + f.getUri() + " at " - + testStep.getCodeLocation(), - resultAtomicReference.get().getResult().getError()); - } - } - }))); - - if (tests.size() > 1) { - features.add(DynamicContainer.dynamicContainer(f.getName().orElse(f.getSource()), tests.stream())); - } - }); - - features.add(DynamicTest.dynamicTest("Finish Cucumber", context::finishTestRun)); - - return features; - } - - public static class CdiObjectFactory implements ObjectFactory { - public CdiObjectFactory() { - } - - public void start() { - - } - - public void stop() { - - } - - public boolean addClass(Class clazz) { - return true; - } - - public T getInstance(Class type) { - var old = Thread.currentThread().getContextClassLoader(); - try { - Thread.currentThread().setContextClassLoader(type.getClassLoader()); - Instance selected = CDI.current().select(type); - if (selected.isUnsatisfied()) { - throw new IllegalArgumentException(type.getName() + " is no CDI bean."); - } else { - return selected.get(); - } - } finally { - Thread.currentThread().setContextClassLoader(old); - } - } - } - - protected static void runMain(Class testClass, String[] args) { - RuntimeOptions propertiesFileOptions = new CucumberPropertiesParser() - .parse(CucumberProperties.fromPropertiesFile()) - .build(); - - RuntimeOptions environmentOptions = new CucumberPropertiesParser() - .parse(CucumberProperties.fromEnvironment()) - .build(propertiesFileOptions); - - RuntimeOptions systemOptions = new CucumberPropertiesParser() - .parse(CucumberProperties.fromSystemProperties()) - .build(environmentOptions); - - CommandlineOptionsParser commandlineOptionsParser = new CommandlineOptionsParser(System.out); - RuntimeOptions runtimeOptions = commandlineOptionsParser.parse(args).build(systemOptions); - - commandlineOptionsParser.exitStatus().ifPresent(System::exit); - - System.setProperty(Constants.ANSI_COLORS_DISABLED_PROPERTY_NAME, String.valueOf(runtimeOptions.isMonochrome())); - //TODO: CUCUMBER_PROPERTIES_FILE_NAME - System.setProperty(Constants.EXECUTION_DRY_RUN_PROPERTY_NAME, String.valueOf(runtimeOptions.isDryRun())); - System.setProperty(Constants.EXECUTION_LIMIT_PROPERTY_NAME, String.valueOf(runtimeOptions.getLimitCount())); - //TODO: EXECUTION_ORDER_PROPERTY_NAME runtimeOptions.getPickleOrder(); (how can we convert this?) - //--strict/--no-strict is already handled by the CommandlineOptionsParser EXECUTION_STRICT_PROPERTY_NAME - System.setProperty(Constants.WIP_PROPERTY_NAME, String.valueOf(runtimeOptions.isWip())); - System.setProperty(Constants.FEATURES_PROPERTY_NAME, - runtimeOptions.getFeaturePaths().stream().map(URI::toString).collect(Collectors.joining(","))); - System.setProperty(Constants.FILTER_NAME_PROPERTY_NAME, - runtimeOptions.getNameFilters().stream().map(Pattern::toString).collect(Collectors.joining(","))); - System.setProperty(Constants.FILTER_TAGS_PROPERTY_NAME, - runtimeOptions.getTagExpressions().stream().map(Object::toString).collect(Collectors.joining(","))); - System.setProperty(Constants.GLUE_PROPERTY_NAME, - runtimeOptions.getGlue().stream().map(URI::toString).collect(Collectors.joining(","))); - Optional.ofNullable(runtimeOptions.getObjectFactoryClass()) - .ifPresent(s -> System.setProperty(Constants.OBJECT_FACTORY_PROPERTY_NAME, s.getName())); - System.setProperty(Constants.PLUGIN_PROPERTY_NAME, - runtimeOptions.plugins().stream().map(Options.Plugin::pluginString).collect(Collectors.joining(","))); - //Not supported via CLI argument: PLUGIN_PUBLISH_ENABLED_PROPERTY_NAME - //Not supported via CLI argument: PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME - //Not supported via CLI argument: PLUGIN_PUBLISH_URL_PROPERTY_NAME - //Not supported via CLI argument: PLUGIN_PUBLISH_QUIET_PROPERTY_NAME - System.setProperty(Constants.SNIPPET_TYPE_PROPERTY_NAME, runtimeOptions.getSnippetType().toString().toLowerCase()); - - ConsoleLauncher.main("-c", testClass.getName()); - } -} +public abstract class CucumberQuarkusTest extends CucumberBaseTest { +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/cucumber/QuarkusCucumberOptionsProvider.java b/runtime/src/main/java/io/quarkiverse/cucumber/QuarkusCucumberOptionsProvider.java index e7b99dc..89aaa85 100644 --- a/runtime/src/main/java/io/quarkiverse/cucumber/QuarkusCucumberOptionsProvider.java +++ b/runtime/src/main/java/io/quarkiverse/cucumber/QuarkusCucumberOptionsProvider.java @@ -9,22 +9,20 @@ * Derived from JUnit4 Cucumber options provider. */ public class QuarkusCucumberOptionsProvider implements CucumberOptionsAnnotationParser.OptionsProvider { - @Override public CucumberOptionsAnnotationParser.CucumberOptions getOptions(Class clazz) { if (hasOptions(clazz)) { CucumberOptions annotation = clazz.getAnnotation(CucumberOptions.class); return new QuarkusCucumberOptionsProvider.QuarkusCucumberOptions(annotation); } - return null; } /** * Checks if {@link io.quarkiverse.cucumber.CucumberOptions} annotation is present on given class. * - * @param clazz - * @return + * @param clazz to check + * @return {@code true} if class has options, {@code false} otherwise */ public boolean hasOptions(Class clazz) { return clazz.getAnnotation(CucumberOptions.class) != null; @@ -34,7 +32,6 @@ public boolean hasOptions(Class clazz) { * Options implementation using given annotation to retrieve Cucumber settings. */ private static class QuarkusCucumberOptions implements CucumberOptionsAnnotationParser.CucumberOptions { - private final CucumberOptions annotation; QuarkusCucumberOptions(CucumberOptions annotation) { @@ -95,6 +92,5 @@ public SnippetType snippets() { public Class objectFactory() { return annotation.objectFactory(); } - } }