├── groovy ├── .gitignore ├── bin │ ├── .gitignore │ └── cucumber-jvm.groovy ├── src │ ├── test │ │ ├── resources │ │ │ ├── junit-platform.properties │ │ │ └── io │ │ │ │ └── cucumber │ │ │ │ └── groovy │ │ │ │ ├── issue_297.feature │ │ │ │ ├── issue_297_stepdefs.groovy │ │ │ │ ├── interpreted_stepdefs.groovy │ │ │ │ ├── data-table.feature │ │ │ │ └── a_feature.feature │ │ ├── groovy │ │ │ └── io │ │ │ │ └── cucumber │ │ │ │ └── groovy │ │ │ │ ├── WorldWithPropertyAndMethod.groovy │ │ │ │ ├── AnotherCustomWorld.groovy │ │ │ │ ├── CustomWorld.groovy │ │ │ │ ├── ExceptionThrowingThing.groovy │ │ │ │ ├── Person.groovy │ │ │ │ ├── second_world_stepdefs.groovy │ │ │ │ ├── date_stepdefs.groovy │ │ │ │ ├── steps.groovy │ │ │ │ ├── Author.groovy │ │ │ │ └── compiled_stepdefs.groovy │ │ └── java │ │ │ └── io │ │ │ └── cucumber │ │ │ └── groovy │ │ │ ├── RunCukesTest.java │ │ │ ├── TestFeatureParser.java │ │ │ ├── ParallelTest.java │ │ │ ├── GroovyStackTraceTest.java │ │ │ ├── HooksTest.java │ │ │ ├── GroovyWorldTest.java │ │ │ ├── GroovyBackendTest.java │ │ │ └── GroovySnippetTest.java │ └── main │ │ ├── resources │ │ └── META-INF.services │ │ │ └── io.cucumber.core.backend.BackendProviderService │ │ ├── java │ │ └── io │ │ │ └── cucumber │ │ │ └── groovy │ │ │ ├── Status.java │ │ │ ├── PendingException.java │ │ │ ├── InvalidMethodException.java │ │ │ ├── GroovyBackendProviderService.java │ │ │ ├── DefaultParameterTransformer.java │ │ │ ├── DocStringType.java │ │ │ ├── AbstractStepGlueDefinition.java │ │ │ ├── AbstractParamGlueDefinition.java │ │ │ ├── DefaultDataTableCellTransformer.java │ │ │ ├── GroovyStepDefinition.java │ │ │ ├── Transpose.java │ │ │ ├── GroovyHookDefinition.java │ │ │ ├── DataTableType.java │ │ │ ├── DefaultDataTableEntryTransformer.java │ │ │ ├── GroovySnippet.java │ │ │ ├── GroovyDocStringTypeDefinition.java │ │ │ ├── GroovyDefaultParameterTransformerDefinition.java │ │ │ ├── MethodFormat.java │ │ │ ├── GroovyScriptIdentifier.java │ │ │ ├── ParameterInfoGroovy.java │ │ │ ├── GroovyDefaultDataTableCellTransformerDefinition.java │ │ │ ├── ScriptPath.java │ │ │ ├── MethodScanner.java │ │ │ ├── AbstractDatatableElementTransformerDefinition.java │ │ │ ├── InvalidMethodSignatureException.java │ │ │ ├── ParameterType.java │ │ │ ├── GroovyParameterTypeDefinition.java │ │ │ ├── Invoker.java │ │ │ ├── Hooks.java │ │ │ ├── GroovyWorld.java │ │ │ ├── Scenario.java │ │ │ ├── GroovyDataTableTypeDefinition.java │ │ │ ├── GroovyDefaultDataTableEntryTransformerDefinition.java │ │ │ └── GroovyBackend.java │ │ └── codegen │ │ ├── generate-keywords.groovy │ │ └── I18n.groovy.gsp └── pom.xml ├── examples ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ ├── junit-platform.properties │ │ │ └── calc │ │ │ │ └── division.feature │ │ ├── java │ │ │ └── calc │ │ │ │ └── RunCukesTest.java │ │ └── groovy │ │ │ └── calc │ │ │ └── CalculatorSteps.groovy │ └── main │ │ └── java │ │ └── calc │ │ └── Calculator.java ├── README.md └── pom.xml ├── .github ├── FUNDING.yml ├── renovate.json ├── stale.yml ├── lock.yml └── workflows │ └── test-java.yml ├── .gitignore ├── LICENCE ├── README.md ├── docs ├── usage.md ├── step_definitions.md ├── world.md ├── groovy_implementation.md ├── install.md ├── hooks.md ├── datatables.md └── transformers.md ├── pom.xml └── CHANGELOG.md /groovy/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target 3 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: cucumber 2 | -------------------------------------------------------------------------------- /groovy/bin/.gitignore: -------------------------------------------------------------------------------- 1 | cucumber-groovy-shaded.jar 2 | -------------------------------------------------------------------------------- /groovy/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | cucumber.publish.quiet=true 2 | -------------------------------------------------------------------------------- /examples/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | cucumber.publish.quiet=true 2 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Groovy Example 2 | 3 | copied from cuke4duke groovy example 4 | 5 | 6 | ## Running Example 7 | 8 | mvn test 9 | -------------------------------------------------------------------------------- /groovy/src/main/resources/META-INF.services/io.cucumber.core.backend.BackendProviderService: -------------------------------------------------------------------------------- 1 | io.cucumber.groovy.GroovyBackendProviderService -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | 4 | target 5 | 6 | dependency-reduced-pom.xml 7 | pom.xml.releaseBackup 8 | release.properties 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>cucumber/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /groovy/src/test/resources/io/cucumber/groovy/issue_297.feature: -------------------------------------------------------------------------------- 1 | Feature: Issue 297 2 | 3 | Scenario: Carbon Coder executes unsuccessfully 4 | Given Carbon Coder is running correctly 5 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/WorldWithPropertyAndMethod.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | class WorldWithPropertyAndMethod { 4 | def aProperty 5 | 6 | void aMethod(List args) {} 7 | } 8 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/Status.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | @API(status = API.Status.STABLE) 6 | public enum Status { 7 | PASSED, 8 | SKIPPED, 9 | PENDING, 10 | UNDEFINED, 11 | AMBIGUOUS, 12 | FAILED, 13 | UNUSED 14 | } 15 | -------------------------------------------------------------------------------- /groovy/bin/cucumber-jvm.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | __directory = new File(getClass().getProtectionDomain().getCodeSource().getLocation().getPath()).getParent(); 3 | this.class.classLoader.addURL(new File(__directory, "cucumber-groovy-shaded.jar").toURL()) 4 | this.class.classLoader.loadClass("io.cucumber.core.cli.Main").main(args) 5 | -------------------------------------------------------------------------------- /groovy/src/test/resources/io/cucumber/groovy/issue_297_stepdefs.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | import io.cucumber.groovy.EN 4 | 5 | this.metaClass.mixin(EN) 6 | 7 | // Step definitions without parameters must explicitly define an empty list of parameters. 8 | Given(~/Carbon Coder is running correctly$/) { -> 9 | } 10 | -------------------------------------------------------------------------------- /examples/src/main/java/calc/Calculator.java: -------------------------------------------------------------------------------- 1 | package calc; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Calculator { 7 | List stack = new ArrayList(); 8 | 9 | public void push(double arg) { 10 | stack.add(arg); 11 | } 12 | 13 | public double divide() { 14 | return stack.get(0) / stack.get(1); 15 | } 16 | } -------------------------------------------------------------------------------- /groovy/src/test/resources/io/cucumber/groovy/interpreted_stepdefs.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | import io.cucumber.groovy.EN 4 | 5 | import java.util.regex.Pattern 6 | 7 | this.metaClass.mixin(Hooks) 8 | this.metaClass.mixin(EN) 9 | 10 | Given([~/^I have (\d+) cuke in my belly/, ~/^I have (\d+) cukes in my belly/] as Pattern[]) { int cukes -> 11 | haveCukes(cukes) 12 | lastAte('cukes') 13 | } 14 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/PendingException.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.backend.Pending; 4 | 5 | @Pending 6 | public final class PendingException extends RuntimeException { 7 | 8 | public PendingException() { 9 | this("TODO: implement me"); 10 | } 11 | 12 | public PendingException(String message) { 13 | super(message); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/AnotherCustomWorld.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | class AnotherCustomWorld { 4 | def aProperty 5 | def methodArgs 6 | 7 | def aMethod() { 8 | methodArgs = "no args" 9 | } 10 | 11 | def aMethod(List args) { 12 | methodArgs = args 13 | } 14 | 15 | def getPropertyValue() { 16 | aProperty 17 | } 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/CustomWorld.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | class CustomWorld { 4 | private def cukes 5 | def lastAte 6 | 7 | def haveCukes(n) { 8 | cukes = n 9 | } 10 | 11 | def checkCukes(n) { 12 | assertEquals(cukes, n) 13 | } 14 | 15 | def lastAte(food) { 16 | lastAte = food 17 | } 18 | 19 | def getMood() { 20 | 'cukes'.equals(lastAte) ? 'happy' : 'tired' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/ExceptionThrowingThing.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | class ExceptionThrowingThing { 4 | def methodMissing(String name, args) { 5 | throw new RuntimeException("Don't have method $name taking $args") 6 | } 7 | 8 | RuntimeException returnGroovyException() { 9 | try { 10 | this.foo() 11 | null 12 | } catch (RuntimeException e) { 13 | e 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/src/test/java/calc/RunCukesTest.java: -------------------------------------------------------------------------------- 1 | package calc; 2 | 3 | 4 | import org.junit.platform.suite.api.ConfigurationParameter; 5 | import org.junit.platform.suite.api.IncludeEngines; 6 | import org.junit.platform.suite.api.SelectClasspathResource; 7 | import org.junit.platform.suite.api.Suite; 8 | 9 | import static io.cucumber.core.options.Constants.GLUE_PROPERTY_NAME; 10 | 11 | @Suite 12 | @IncludeEngines("cucumber") 13 | @SelectClasspathResource("calc") 14 | @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "calc") 15 | public class RunCukesTest { 16 | } 17 | -------------------------------------------------------------------------------- /examples/src/test/resources/calc/division.feature: -------------------------------------------------------------------------------- 1 | # language: en 2 | Feature: Division 3 | In order to avoid silly mistakes 4 | Cashiers must be able to calculate a fraction 5 | 6 | @important 7 | Scenario: Regular numbers 8 | Given I have entered 3 into the calculator 9 | And I have entered 2 into the calculator 10 | When I press divide 11 | Then the stored result should be 1.5 12 | 13 | Scenario: More numbers 14 | Given I have entered 6 into the calculator 15 | And I have entered 3 into the calculator 16 | When I press divide 17 | Then the stored result should be 2.0 -------------------------------------------------------------------------------- /groovy/src/test/java/io/cucumber/groovy/RunCukesTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.junit.platform.suite.api.ConfigurationParameter; 4 | import org.junit.platform.suite.api.IncludeEngines; 5 | import org.junit.platform.suite.api.SelectClasspathResource; 6 | import org.junit.platform.suite.api.Suite; 7 | 8 | import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; 9 | 10 | @Suite 11 | @IncludeEngines("cucumber") 12 | @SelectClasspathResource("io/cucumber/groovy") 13 | @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "io.cucumber.groovy") 14 | public class RunCukesTest { 15 | } 16 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/InvalidMethodException.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.backend.CucumberBackendException; 4 | 5 | import java.lang.reflect.Method; 6 | 7 | final class InvalidMethodException extends CucumberBackendException { 8 | 9 | private InvalidMethodException(String message) { 10 | super(message); 11 | } 12 | 13 | static InvalidMethodException createInvalidMethodException(Method method, Class glueCodeClass) { 14 | return new InvalidMethodException( 15 | "You're not allowed to extend classes that define Step Definitions or hooks. " 16 | + glueCodeClass + " extends " + method.getDeclaringClass()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/GroovyBackendProviderService.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import com.google.auto.service.AutoService; 4 | import io.cucumber.core.backend.Backend; 5 | import io.cucumber.core.backend.BackendProviderService; 6 | import io.cucumber.core.backend.Container; 7 | import io.cucumber.core.backend.Lookup; 8 | 9 | import java.util.function.Supplier; 10 | 11 | @AutoService(BackendProviderService.class) 12 | public final class GroovyBackendProviderService implements BackendProviderService { 13 | @Override 14 | public Backend create(Lookup lookup, Container container, Supplier classLoaderSupplier) { 15 | return new GroovyBackend(lookup, container, classLoaderSupplier); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/Person.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | class Person { 4 | private final String first 5 | private final String last 6 | 7 | Person(String first, String last) { 8 | this.first = first 9 | this.last = last 10 | } 11 | 12 | boolean equals(o) { 13 | if (this.is(o)) return true 14 | if (getClass() != o.class) return false 15 | 16 | Person person = (Person) o 17 | 18 | if (first != person.first) return false 19 | if (last != person.last) return false 20 | 21 | return true 22 | } 23 | 24 | int hashCode() { 25 | int result 26 | result = (first != null ? first.hashCode() : 0) 27 | result = 31 * result + (last != null ? last.hashCode() : 0) 28 | return result 29 | } 30 | } -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/DefaultParameterTransformer.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | 11 | /** 12 | * Register default parameter type transformer. 13 | *

14 | * Valid method signatures are: 15 | *

    16 | *
  • {@code String, Type -> Object}
  • 17 | *
  • {@code Object, Type -> Object}
  • 18 | *
19 | * 20 | * @see io.cucumber.cucumberexpressions.ParameterByTypeTransformer 21 | * @see io.cucumber.cucumberexpressions.ParameterType 22 | */ 23 | 24 | @Retention(RetentionPolicy.RUNTIME) 25 | @Target(ElementType.METHOD) 26 | @API(status = API.Status.STABLE) 27 | public @interface DefaultParameterTransformer { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /groovy/src/test/resources/io/cucumber/groovy/data-table.feature: -------------------------------------------------------------------------------- 1 | Feature: Datatable 2 | 3 | 4 | 5 | Scenario: Convert a table to a generic list via the ParameterTypeRegistry 6 | Given a list of authors in a table 7 | | firstName | lastName | birthDate | 8 | | Annie M. G. | Schmidt | 1911-03-20 | 9 | | Roald | Dahl | 1916-09-13 | 10 | 11 | 12 | Scenario: Convert a table to a single object via the ParameterTypeRegistry 13 | 14 | Given a single author in a table 15 | | firstName | lastName | birthDate | 16 | | Annie M. G. | Schmidt | 1911-03-20 | 17 | 18 | 19 | 20 | 21 | Scenario: Convert a table to a generic list via the @DataTableType Annotation 22 | 23 | Given a list of people in a table 24 | | first | last | 25 | | Astrid | Lindgren | 26 | | Roald | Dahl | 27 | | Plato | [blank] | 28 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 300 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 60 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - ":safety_pin: pinned" 8 | # Label to use when marking an issue as stale 9 | staleLabel: ":hourglass: stale" 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed in two months if no further activity occurs. 14 | # Comment to post when closing a stale issue. Set to `false` to disable 15 | closeComment: > 16 | This issue has been automatically closed because of inactivity. 17 | You can support the Cucumber core team on [opencollective](https://opencollective.com/cucumber). 18 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) The Cucumber Organisation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /groovy/src/main/codegen/generate-keywords.groovy: -------------------------------------------------------------------------------- 1 | import groovy.text.SimpleTemplateEngine 2 | import io.cucumber.gherkin.GherkinDialects 3 | 4 | def engine = new SimpleTemplateEngine() 5 | def templateSource = new File(project.baseDir, "${File.separator}src${File.separator}main${File.separator}codegen${File.separator}I18n.groovy.gsp").getText() 6 | 7 | def unsupported = ["EM"] // The generated files for Emoij do not compile. 8 | 9 | GherkinDialects.getDialects().each { dialect -> 10 | def normalized_language = dialect.language.replaceAll("[\\s-]", "_").toUpperCase() 11 | if (!unsupported.contains(normalized_language)) { 12 | def binding = ["i18n":dialect, "normalized_language":normalized_language] 13 | template = engine.createTemplate(templateSource).make(binding) 14 | def file = new File(project.baseDir, "target${File.separator}generated-sources${File.separator}i18n${File.separator}java${File.separator}io${File.separator}cucumber${File.separator}groovy${File.separator}${normalized_language}.java") 15 | file.parentFile.mkdirs() 16 | file.write(template.toString(), "UTF-8") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for lock-threads - https://github.com/dessant/lock-threads 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 365 5 | 6 | # Issues and pull requests with these labels will not be locked. Set to `[]` to disable 7 | exemptLabels: [] 8 | 9 | # Label to add before locking, such as `outdated`. Set to `false` to disable 10 | lockLabel: false 11 | 12 | # Comment to post before locking. Set to `false` to disable 13 | lockComment: > 14 | This thread has been automatically locked since there has not been 15 | any recent activity after it was closed. Please open a new issue for 16 | related bugs. 17 | 18 | # Assign `resolved` as the reason for locking. Set to `false` to disable 19 | setLockReason: true 20 | 21 | # Limit to only `issues` or `pulls` 22 | # only: issues 23 | 24 | # Optionally, specify configuration settings just for `issues` or `pulls` 25 | # issues: 26 | # exemptLabels: 27 | # - help-wanted 28 | # lockLabel: outdated 29 | 30 | # pulls: 31 | # daysUntilLock: 30 32 | 33 | # Repository to extend settings from 34 | # _extends: repo 35 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/second_world_stepdefs.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | import io.cucumber.datatable.DataTable 4 | import io.cucumber.groovy.EN 5 | 6 | this.metaClass.mixin(EN) 7 | this.metaClass.mixin(Hooks) 8 | 9 | def topLevelValueWrite = 100 10 | def topLevelValueRead = "TOP" 11 | 12 | 13 | World { 14 | new AnotherCustomWorld() 15 | } 16 | 17 | 18 | When(~/^set world property "(\w+)"$/) { String p -> 19 | aProperty = p 20 | topLevelValueWrite = p 21 | } 22 | 23 | Then(~/^properties visibility is ok$/) { -> 24 | assert topLevelValueWrite && topLevelValueWrite != 100 25 | assert topLevelValueRead == "TOP" 26 | } 27 | 28 | Then(~/^world property is "(\w+)"$/) { String p -> 29 | assert aProperty == p 30 | assert propertyValue == p 31 | assert topLevelValueWrite == p 32 | } 33 | 34 | When(~/^world method call$/) { -> 35 | aMethod() 36 | } 37 | 38 | When(~/^world method call:$/) { DataTable table -> 39 | aMethod(table.asList(Integer)) 40 | } 41 | 42 | Then(~/^world method call is:$/) { DataTable table -> 43 | methodArgs == table.asList(Integer) 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/DocStringType.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Register doc string type. 12 | *

13 | * The name of the method is used as the content type of the 14 | * {@link io.cucumber.docstring.DocStringType}. 15 | *

16 | * The method must have this signature: 17 | *

    18 | *
  • {@code String -> Author}
  • 19 | *
20 | * NOTE: {@code Author} is an example of the type of the parameter type. 21 | * 22 | * @see io.cucumber.docstring.DocStringType 23 | */ 24 | @Retention(RetentionPolicy.RUNTIME) 25 | @Target(ElementType.METHOD) 26 | @API(status = API.Status.STABLE) 27 | public @interface DocStringType { 28 | 29 | /** 30 | * Name of the content type. 31 | *

32 | * When not provided this will default to the name of the annotated method. 33 | * 34 | * @return content type 35 | */ 36 | String contentType() default ""; 37 | } 38 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/AbstractStepGlueDefinition.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import groovy.lang.Closure; 4 | import io.cucumber.core.backend.Located; 5 | import io.cucumber.core.backend.ParameterInfo; 6 | 7 | import java.util.List; 8 | 9 | abstract class AbstractStepGlueDefinition implements Located { 10 | 11 | protected final Closure body; 12 | private final StackTraceElement location; 13 | 14 | AbstractStepGlueDefinition(Closure body, StackTraceElement location) { 15 | this.location = location; 16 | this.body = body; 17 | } 18 | 19 | @Override 20 | public String getLocation() { 21 | return location.getFileName() + ":" + location.getLineNumber(); 22 | } 23 | 24 | @Override 25 | public boolean isDefinedAt(StackTraceElement stackTraceElement) { 26 | return location.getFileName().equals(stackTraceElement.getFileName()); 27 | } 28 | 29 | List getParameterInfos() { 30 | return ParameterInfoGroovy.fromTypes(body); 31 | } 32 | 33 | List getParameterInfosHooks() { 34 | return ParameterInfoGroovy.fromTypesHooks(body); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Maven Dependency 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/io.cucumber/cucumber-groovy.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.cucumber%22%20AND%20a:%22cucumber-groovy%22) 4 | 5 | Cucumber Groovy is the Groovy implementation of [Cucumber](https://cucumber.io/). 6 | 7 | ## Help & Support 8 | 9 | See: https://cucumber.io/support 10 | 11 | ## Getting started 12 | 13 | - [Installation](./docs/install.md) 14 | - Documentation 15 | - [Basic usage](docs/usage.md) 16 | - [Step Definitions](docs/step_definitions.md) 17 | - [DataTables](docs/datatables.md) 18 | - [Hooks](docs/hooks.md) 19 | - [Transformers](docs/transformers.md) 20 | - [World](docs/world.md) 21 | - [Example project](examples/README.md) 22 | - [Reference documentation for Java](https://docs.cucumber.io/docs/cucumber/) 23 | - [Changelog](CHANGELOG.md) 24 | 25 | ## They are using it 26 | 27 | You are using Cucumber Groovy? We would love to know about you! Please open a PR to add your project or company to the list below. 28 | 29 | 30 | ## Contributing 31 | 32 | To build the project run: 33 | 34 | ```shell 35 | mvn verify 36 | ``` -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/AbstractParamGlueDefinition.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.backend.Located; 4 | import io.cucumber.core.backend.Lookup; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | import static java.util.Objects.requireNonNull; 9 | 10 | abstract class AbstractParamGlueDefinition implements Located { 11 | 12 | protected final Method method; 13 | protected final Lookup lookup; 14 | private String fullFormat; 15 | 16 | AbstractParamGlueDefinition(Method method, Lookup lookup) { 17 | this.method = requireNonNull(method); 18 | this.lookup = requireNonNull(lookup); 19 | } 20 | 21 | @Override 22 | public boolean isDefinedAt(StackTraceElement e) { 23 | return e.getClassName().equals(method.getDeclaringClass().getName()) && e.getMethodName().equals(method.getName()); 24 | } 25 | 26 | @Override 27 | public final String getLocation() { 28 | return getFullLocationLocation(); 29 | } 30 | 31 | private String getFullLocationLocation() { 32 | if (fullFormat == null) { 33 | fullFormat = MethodFormat.FULL.format(method); 34 | } 35 | return fullFormat; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/test-java.yml: -------------------------------------------------------------------------------- 1 | name: Test Java 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | - renovate/** 11 | 12 | jobs: 13 | build: 14 | name: 'Build' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v6 18 | - uses: actions/setup-java@v5 19 | with: 20 | distribution: "temurin" 21 | java-version: 17 22 | cache: "maven" 23 | - name: Install dependencies 24 | run: mvn install -DskipTests=true -DskipITs=true-Dmaven.javadoc.skip=true --batch-mode -Dstyle.color=always --show-version 25 | - name: Test 26 | run: mvn verify -Dstyle.color=always 27 | 28 | javadoc: 29 | name: 'Javadoc' 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v6 33 | - uses: actions/setup-java@v5 34 | with: 35 | distribution: "temurin" 36 | java-version: 17 37 | cache: "maven" 38 | - name: Install dependencies 39 | run: mvn install -DskipTests=true -DskipITs=true -Dmaven.javadoc.skip=true --batch-mode -Dstyle.color=always --show-version 40 | - name: Javadoc 41 | run: mvn javadoc:jar -Dstyle.color=always 42 | -------------------------------------------------------------------------------- /groovy/src/test/java/io/cucumber/groovy/TestFeatureParser.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.feature.FeatureIdentifier; 4 | import io.cucumber.core.feature.FeatureParser; 5 | import io.cucumber.core.gherkin.Feature; 6 | import io.cucumber.core.resource.Resource; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.InputStream; 10 | import java.net.URI; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.UUID; 13 | 14 | class TestFeatureParser { 15 | static Feature parse(final String source) { 16 | return parse("file:test.feature", source); 17 | } 18 | 19 | private static Feature parse(final String uri, final String source) { 20 | return parse(FeatureIdentifier.parse(uri), source); 21 | } 22 | 23 | private static Feature parse(final URI uri, final String source) { 24 | return new FeatureParser(UUID::randomUUID).parseResource(new Resource() { 25 | @Override 26 | public URI getUri() { 27 | return uri; 28 | } 29 | 30 | @Override 31 | public InputStream getInputStream() { 32 | return new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)); 33 | } 34 | 35 | }).orElse(null); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/date_stepdefs.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 5 | 6 | import java.lang.reflect.Type 7 | import java.time.LocalDate 8 | 9 | import static groovy.test.GroovyTestCase.assertEquals 10 | 11 | this.metaClass.mixin(Hooks) 12 | this.metaClass.mixin(EN) 13 | 14 | class DateWrapper { 15 | def date 16 | } 17 | 18 | 19 | @ParameterType("([0-9]{4})-([0-9]{2})-([0-9]{2})") 20 | LocalDate parameterTypeIso8601Date(String year, String month, String day) { 21 | LocalDate.of(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day)) 22 | } 23 | 24 | @DefaultParameterTransformer 25 | Object anonymous(String fromValue, Type toValueType) { 26 | ObjectMapper objectMapper = new ObjectMapper() 27 | objectMapper.registerModule(new JavaTimeModule()) 28 | objectMapper.convertValue(fromValue, objectMapper.constructType(toValueType)); 29 | } 30 | 31 | 32 | Given('today\'s date is "{parameterTypeIso8601Date}" and tomorrow is:') { LocalDate today, String tomorrow -> 33 | assertEquals(3, today.getDayOfMonth()) 34 | assertEquals('1971-10-04',tomorrow) 35 | } 36 | 37 | And('anonymous date is {}') { LocalDate parsedDate -> 38 | assertEquals( LocalDate.of(2011, 1, 19), parsedDate) 39 | } 40 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/DefaultDataTableCellTransformer.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Register default data table cell transformer. 12 | *

13 | * Valid method signatures are: 14 | *

    15 | *
  • {@code String, Type -> Object}
  • 16 | *
  • {@code Object, Type -> Object}
  • 17 | *
18 | * 19 | * @see io.cucumber.datatable.TableCellByTypeTransformer 20 | * @see io.cucumber.datatable.DataTableType 21 | */ 22 | @Retention(RetentionPolicy.RUNTIME) 23 | @Target(ElementType.METHOD) 24 | @API(status = API.Status.STABLE) 25 | public @interface DefaultDataTableCellTransformer { 26 | 27 | /** 28 | * Replace these strings in the Datatable with empty strings. 29 | *

30 | * A data table can only represent absent and non-empty strings. By replacing 31 | * a known value (for example [empty]) a data table can also represent 32 | * empty strings. 33 | *

34 | * It is not recommended to use multiple replacements in the same table. 35 | * 36 | * @return strings to be replaced with empty strings. 37 | */ 38 | String[] replaceWithEmptyString() default {}; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/steps.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertEquals 5 | import static org.junit.jupiter.api.Assertions.assertTrue 6 | 7 | this.metaClass.mixin(EN) 8 | 9 | final Author expectedAuthor = new Author("Annie M. G.", "Schmidt", "1911-03-20") 10 | final Person expectedPerson = new Person("Astrid", "Lindgren") 11 | final Person mononymousPerson = new Person("Plato", "") 12 | 13 | @DataTableType 14 | static Author authorEntryTransformer(Map entry) { 15 | return new Author( 16 | entry.get("firstName"), 17 | entry.get("lastName"), 18 | entry.get("birthDate")) 19 | } 20 | 21 | Given("a list of authors in a table") { List authors -> 22 | assertTrue(authors.contains(expectedAuthor)) 23 | } 24 | 25 | 26 | Given("a single author in a table") { 27 | Author author -> 28 | assertEquals(expectedAuthor, author) 29 | } 30 | 31 | 32 | Given("a list of people in a table") { List persons -> 33 | assertTrue(persons.contains(expectedPerson)) 34 | assertTrue(persons.contains(mononymousPerson)) 35 | } 36 | 37 | @DataTableType(replaceWithEmptyString = "[blank]") 38 | static Person transformPerson(Map tableEntry) { 39 | return new Person(tableEntry.get("first"), tableEntry.get("last")) 40 | } 41 | 42 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | ## Glue code 4 | 5 | To use Cucumber Groovy, all your glue code (steps or hooks) has to be defined in groovy scripts importing necessary cucumber DSL. 6 | 7 | For instance, to use the English flavour: 8 | 9 | ```groovy 10 | import io.cucumber.groovy.EN 11 | this.metaClass.mixin(Hooks) 12 | this.metaClass.mixin(EN) 13 | 14 | 15 | Before() { 16 | // Do something before each scenario 17 | } 18 | 19 | 20 | Given(~/'^I have (\d+) cucumbers in my belly$'/){ int cucumberCount -> 21 | // Do something 22 | } 23 | ``` 24 | 25 | Cucumber will automatically load all the glue code defined in scripts available in the "glue path" (more details in the Run documentation). 26 | 27 | 28 | ## Running Cucumber tests 29 | 30 | See also the Running Cucumber for Java [documentation](https://docs.cucumber.io/docs/cucumber/api/#running-cucumber). 31 | 32 | Add the `cucumber-junit` dependency to your project. 33 | 34 | Then create a runner class like this: 35 | ```java 36 | import io.cucumber.junit.Cucumber; 37 | import org.junit.runner.RunWith; 38 | 39 | @RunWith(Cucumber.class) 40 | public class RunCukesTest { 41 | } 42 | 43 | ``` 44 | 45 | You can define several options like: 46 | - the "glue path" (default to current package): packages in which to look for glue code 47 | - the "features path" (default to current folder): folder in which to look for features file 48 | -------------------------------------------------------------------------------- /docs/step_definitions.md: -------------------------------------------------------------------------------- 1 | # Step definitions 2 | 3 | Step definitions (`Given`, `When`, `Then`) are the glue between features written in Gherkin and the actual tests implementation. 4 | 5 | Cucumber supports two types of expressions: 6 | 7 | - Cucumber expressions 8 | - Regular expressions 9 | 10 | See also the [reference documentation](https://docs.cucumber.io/docs/cucumber/step-definitions/#expressions). 11 | 12 | ## Cucumber expressions 13 | 14 | [Cucumber expressions](https://docs.cucumber.io/docs/cucumber/cucumber-expressions/) 15 | 16 | The following Gherkin step: 17 | ```gherkin 18 | Given I have 42 cucumbers in my belly 19 | ``` 20 | 21 | Can be implemented with following Cucumber Expression in Groovy: 22 | ```groovy 23 | Given("I have {int} cucumbers in my belly"){ int cucumberCount -> 24 | // Do something 25 | } 26 | ``` 27 | 28 | ## Regular expressions 29 | 30 | The following Gherkin step: 31 | ```gherkin 32 | Given I have 42 cucumbers in my belly 33 | ``` 34 | 35 | Can be implemented with following Regular Expression in Groovy: 36 | ```groovy 37 | Given(~/'^I have (\d+) cucumbers in my belly$'/){ int cucumberCount -> 38 | // Do something 39 | } 40 | ``` 41 | 42 | To implement multiple gherkin steps in a single cucumber step: 43 | ```groovy 44 | Given([~/^I have (\d+) cuke in my belly/, ~/^I have (\d+) cukes in my belly/] as Pattern[]) { int cukes -> 45 | // Do something 46 | } 47 | ``` -------------------------------------------------------------------------------- /groovy/src/test/resources/io/cucumber/groovy/a_feature.feature: -------------------------------------------------------------------------------- 1 | Feature: Cucumber Runner Rocks 2 | 3 | Scenario: Many cukes 4 | Given I have 12 cukes in my belly 5 | And a big basket with cukes 6 | 7 | Scenario: Few cukes 8 | Given I have 3 cukes in my belly 9 | And I have 5 cukes in my belly 10 | 11 | Scenario Outline: Various things 12 | Given I have in my belly 13 | Then I should be 14 | 15 | Examples: some cukes 16 | | n | what | mood | 17 | | 13 | cukes | happy | 18 | | 4 | apples | tired | 19 | 20 | Scenario: a table 21 | Given the following table: 22 | | year | name | 23 | | 2008 | Cucumber | 24 | | 2012 | Cucumber-JVM | 25 | 26 | Scenario: Test list conversion 27 | Given this should be converted to a list:Cucumber-JVM, Cucumber, Nice 28 | 29 | Scenario: A date 30 | Given today's date is "1971-10-03" and tomorrow is: 31 | """ 32 | 1971-10-04 33 | """ 34 | And anonymous date is 2011-01-19 35 | 36 | 37 | Scenario: Call a method or property from second world 38 | When set world property "Hello" 39 | Then world property is "Hello" 40 | When world method call 41 | And world method call: 42 | | 1 | 43 | | 2 | 44 | | 3 | 45 | Then world method call is: 46 | | 1 | 47 | | 2 | 48 | | 3 | 49 | And properties visibility is ok 50 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/Author.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | class Author { 4 | final String firstName 5 | final String lastName 6 | final birthDate 7 | 8 | Author(String firstName, String lastName, String birthDate) { 9 | this.firstName = firstName 10 | this.lastName = lastName 11 | this.birthDate = birthDate 12 | } 13 | 14 | @Override 15 | String toString() { 16 | return "io.cucumber.groovy.Author{" + 17 | "firstName='" + firstName + '\'' + 18 | ", lastName='" + lastName + '\'' + 19 | ", birthDate='" + birthDate + '\'' + 20 | '}' 21 | } 22 | 23 | @Override 24 | boolean equals(o) { 25 | if (this.is(o)) return true 26 | if (getClass() != o.class) return false 27 | Author author = (Author) o 28 | 29 | if (birthDate != author.birthDate) return false 30 | if (firstName != author.firstName) return false 31 | if (lastName != author.lastName) return false 32 | return true 33 | } 34 | 35 | @Override 36 | int hashCode() { 37 | int result 38 | result = (firstName != null ? firstName.hashCode() : 0) 39 | result = 31 * result + (lastName != null ? lastName.hashCode() : 0) 40 | result = 31 * result + (birthDate != null ? birthDate.hashCode() : 0) 41 | return result 42 | } 43 | } -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/GroovyStepDefinition.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import groovy.lang.Closure; 4 | import io.cucumber.core.backend.CucumberBackendException; 5 | import io.cucumber.core.backend.CucumberInvocationTargetException; 6 | import io.cucumber.core.backend.ParameterInfo; 7 | import io.cucumber.core.backend.StepDefinition; 8 | 9 | import java.util.List; 10 | 11 | import static java.util.Objects.requireNonNull; 12 | 13 | public class GroovyStepDefinition extends AbstractStepGlueDefinition implements StepDefinition { 14 | private final String expression; 15 | private final Closure body; 16 | private final GroovyBackend backend; 17 | 18 | public GroovyStepDefinition(String expression, Closure body, StackTraceElement location, GroovyBackend backend) { 19 | super(body, location); 20 | this.expression = requireNonNull(expression, "cucumber-expression may not be null"); 21 | this.backend = backend; 22 | this.body = body; 23 | } 24 | 25 | @Override 26 | public void execute(final Object[] args) throws CucumberBackendException, CucumberInvocationTargetException { 27 | Invoker.invoke(backend, body, args); 28 | } 29 | 30 | @Override 31 | public List parameterInfos() { 32 | return getParameterInfos(); 33 | } 34 | 35 | @Override 36 | public String getPattern() { 37 | return expression; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /groovy/src/test/java/io/cucumber/groovy/ParallelTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | 4 | import groovy.lang.Closure; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.util.function.Supplier; 11 | 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | public class ParallelTest { 15 | @Mock 16 | Supplier classLoaderSupplier; 17 | @Mock 18 | Closure closure; 19 | 20 | @Test 21 | public void can_have_a_new_backend_on_a_different_thread() { 22 | new GroovyBackend(null, null, classLoaderSupplier); 23 | Thread interactWithBackendThread = new Thread(() -> { 24 | try { 25 | GroovyBackend.getInstance().registerWorld(closure); 26 | } catch (NullPointerException e) { 27 | // This is what we want as there should be no GroovyBackend on this thread 28 | } 29 | }); 30 | runAndWait(interactWithBackendThread); 31 | GroovyBackend.getInstance().registerWorld(closure); 32 | } 33 | 34 | private void runAndWait(Thread interactWithBackendThread) { 35 | interactWithBackendThread.start(); 36 | try { 37 | interactWithBackendThread.join(); 38 | } catch (InterruptedException e) { 39 | throw new RuntimeException("Doh"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/Transpose.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | *

12 | * This annotation can be specified on step definition method parameters to give 13 | * Cucumber a hint to transpose a DataTable. 14 | *

15 | * For example, if you have the following Gherkin step with a table 16 | * 17 | *

18 |  * Given the user is
19 |  *    | firstname	| Roberto	|
20 |  *    | lastname	| Lo Giacco |
21 |  *    | nationality	| Italian	|
22 |  * 
23 | *

24 | * And a data table type to create a User 25 | * 26 | *

27 |  * {@code
28 |  * @DataTableType
29 |  * public User convert(Map entry){
30 |  *    return new User(
31 |  *        entry.get("firstname"),
32 |  *        entry.get("lastname")
33 |  *        entry.get("nationality")
34 |  *   );
35 |  * }
36 |  * }
37 |  * 
38 | *

39 | * Then the following Groovy Step Definition would convert that into an User 40 | * object: 41 | * 42 | *

43 |  * Given("^the user is$"){ @Transpose User user ->
44 |  *     this.user = user;
45 |  * }
46 |  * 
47 | */ 48 | @Retention(RetentionPolicy.RUNTIME) 49 | @Target({ElementType.PARAMETER}) 50 | @API(status = API.Status.STABLE) 51 | public @interface Transpose { 52 | 53 | boolean value() default true; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/GroovyHookDefinition.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | 4 | import groovy.lang.Closure; 5 | import io.cucumber.core.backend.HookDefinition; 6 | import io.cucumber.core.backend.TestCaseState; 7 | 8 | import static java.util.Objects.requireNonNull; 9 | 10 | public class GroovyHookDefinition extends AbstractStepGlueDefinition implements HookDefinition { 11 | private final String tagExpression; 12 | private final int order; 13 | private final GroovyBackend backend; 14 | 15 | public GroovyHookDefinition( 16 | String tagExpression, 17 | int order, 18 | Closure body, 19 | StackTraceElement location, 20 | GroovyBackend backend) { 21 | super(body, location); 22 | this.tagExpression = requireNonNull(tagExpression, "tag-expression may not be null"); 23 | this.order = order; 24 | this.backend = backend; 25 | } 26 | 27 | @Override 28 | public void execute(TestCaseState state) { 29 | Object[] args; 30 | if (getParameterInfosHooks().size() == 1) { 31 | args = new Object[]{new io.cucumber.groovy.Scenario(state)}; 32 | } else { 33 | args = new Object[0]; 34 | } 35 | 36 | Invoker.invoke(backend, body, args); 37 | } 38 | 39 | @Override 40 | public String getTagExpression() { 41 | return tagExpression; 42 | } 43 | 44 | @Override 45 | public int getOrder() { 46 | return order; 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /docs/world.md: -------------------------------------------------------------------------------- 1 | # World 2 | 3 | World is an isolated context for each scenario that allow to share state between cucumber steps. 4 | 5 | See the [reference documentation](https://cucumber.io/docs/cucumber/state/). 6 | 7 | In groovy `World` can be defined in any step definition script as follows: 8 | 9 | ```groovy 10 | World { 11 | new CustomWorld() 12 | } 13 | ``` 14 | 15 | And the `World` object itself making its properties and methods available in step definitions: 16 | 17 | ```groovy 18 | class CustomWorld { 19 | def lastAte 20 | 21 | def lastAte(food) { 22 | lastAte = food 23 | } 24 | 25 | def getMood() { 26 | 'cukes'.equals(lastAte) ? 'happy' : 'tired' 27 | } 28 | } 29 | ``` 30 | 31 | Fo example in the below gherkin where we need to pass `what` value from the datatable from one step to another 32 | 33 | ```gherkin 34 | Scenario Outline: Passing varibales between steps 35 | Given I have in my belly 36 | Then I should be 37 | 38 | Examples: some cukes 39 | | n | what | mood | 40 | | 13 | cukes | happy | 41 | | 4 | apples | tired | 42 | ``` 43 | 44 | In step definitions we can save the value in `CustomWorld` object calling `lastAte('cukes')` 45 | 46 | ```groovy 47 | Given([~/^I have (\d+) cuke in my belly/, ~/^I have (\d+) cukes in my belly/] as Pattern[]) { int cukes -> 48 | lastAte('cukes') 49 | } 50 | ``` 51 | 52 | And check the saved value in subsequent step as follows: 53 | 54 | ```groovy 55 | Then(~'^I should be (.*)') { String mood -> 56 | assertEquals(mood, getMood()) 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /groovy/src/main/codegen/I18n.groovy.gsp: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import groovy.lang.Closure; 4 | import io.cucumber.groovy.*; 5 | import java.util.Arrays; 6 | import java.util.regex.Pattern; 7 | 8 | public class ${normalized_language} { 9 | <% i18n.stepKeywords.findAll { !it.contains('*') && !it.matches("^\\d.*") }.unique().each { kw -> %> 10 | 11 | public static void ${java.text.Normalizer.normalize(kw.replaceAll("[\\s',!\u2019]", ""), java.text.Normalizer.Form.NFC)}(String[] expressions, Closure body) throws Throwable { 12 | Arrays.stream(expressions).forEach(expression-> 13 | GroovyBackend.getInstance().addStepDefinition(expression, body) 14 | ); 15 | } 16 | 17 | public static void ${java.text.Normalizer.normalize(kw.replaceAll("[\\s',!\u2019]", ""), java.text.Normalizer.Form.NFC)}(Pattern[] regexps, Closure body) throws Throwable { 18 | Arrays.stream(regexps).forEach(regexp-> 19 | GroovyBackend.getInstance().addStepDefinition("/" + regexp.toString() + "/", body) 20 | ); 21 | } 22 | 23 | public static void ${java.text.Normalizer.normalize(kw.replaceAll("[\\s',!\u2019]", ""), java.text.Normalizer.Form.NFC)}(String expression, Closure body) throws Throwable { 24 | GroovyBackend.getInstance().addStepDefinition(expression, body); 25 | } 26 | 27 | public static void ${java.text.Normalizer.normalize(kw.replaceAll("[\\s',!\u2019]", ""), java.text.Normalizer.Form.NFC)}(Pattern regexp, Closure body) throws Throwable { 28 | GroovyBackend.getInstance().addStepDefinition("/" + regexp.toString() + "/", body); 29 | } 30 | <% } %> 31 | } 32 | -------------------------------------------------------------------------------- /docs/groovy_implementation.md: -------------------------------------------------------------------------------- 1 | # Groovy implementation details 2 | 3 | This page covers some details about the Cucumber Groovy implementation. 4 | 5 | ## Running a Cucumber test 6 | 7 | ### Backend 8 | 9 | From Cucumber core perspective, the entry point of a Cucumber implementation is what is called "backend". 10 | 11 | The `BackendServiceLoader` core service looks for a `BackendProviderService` implementation. 12 | Ours is defined in the class `GroovyBackendProviderService`. 13 | 14 | The implementing class also has to be registered as a "Java Service" in the `META-INF/services/io.cucumber.core.backend.BackendProviderService` file (in the `resources` folder). 15 | 16 | ### Loading the glue 17 | 18 | When a Cucumber test starts, a Cucumber Runner starts and a `GroovyBackend` instance is created. 19 | The `GroovyBackend` instance will be used for running all the scenarios which are part of the test (defined by the _features path_ and the _glue path_). 20 | 21 | The first thing the Runner does is to "load the glue", that is find all the hooks and step definitions and register them. 22 | This is handled by the `GroovyBackend#loadGlue()` method. 23 | 24 | #### Groovy implementation 25 | 26 | In the Cucumber Groovy implementation, loading the glue code means: 27 | - finding all the compiled **scripts** in the _glue path_, and for each: 28 | - extract the hooks and step definitions from it 29 | - add it to the `Container` instance provided by Cucumber Core 30 | - finding all the interpreted **scripts** in the _glue path_ and for each: 31 | - extract the hooks and step definitions from it 32 | - add it to the `Container` instance provided by Cucumber Core 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Dependency 4 | 5 | ### Gradle 6 | 7 | To use Cucumber Groovy in your project, add the following line to your `build.gradle`: 8 | 9 | ``` 10 | testCompile group: 'io.cucumber', name:'cucumber-groovy', version: '5.1.3' 11 | ``` 12 | 13 | ### Maven 14 | 15 | To use Cucumber Groovy in your project, add the following dependency to your `pom.xml`: 16 | 17 | ```xml 18 | 19 | io.cucumber 20 | cucumber-groovy 21 | 5.1.3 22 | test 23 | 24 | ``` 25 | 26 | ### Running from the Command Line Interface (CLI) 27 | 28 | To run the test from the cli call: 29 | 30 | groovy -cp "target/test-classes;./bin/cucumber-groovy-shaded.jar" ./bin/cucumber-jvm.groovy --glue classpath:cucumber/runtime/groovy src/test/resources/cucumber/runtime/groovy/a_feature.feature 31 | 32 | or 33 | 34 | groovy -cp "target/test-classes;./bin/cucumber-groovy-shaded.jar" ./bin/cucumber-jvm.groovy --glue classpath:cucumber.runtime.groovy --glue src/test/resources src/test/resources/cucumber/runtime/groovy/a_feature.feature 35 | 36 | The test uses a mix of compiled and interpreted step definitions which makes the command a bit tricky: 37 | 38 | 1. `-cp target/test-classes;./bin/cucumber-groovy-shaded.jar` tells groovy where to find the compiled class files and needed dependencies for `TypeRegistryConfigurer` descendants. 39 | 2. `--glue classpath:cucumber/runtime/groovy` or `--glue classpath:cucumber.runtime.groovy --glue src/test/resources` is required so that cucumber finds the compiled step definitions 40 | 3. The last parameter provides a feature or a path with features 41 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/DataTableType.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Register a data table type. 12 | *

13 | * The signature of the method is used to determine which data table type is registered: 14 | * 15 | *

    16 | *
  • {@code String -> Author} will register a {@link io.cucumber.datatable.TableCellTransformer}
  • 17 | *
  • {@code Map -> Author} will register a {@link io.cucumber.datatable.TableEntryTransformer}
  • 18 | *
  • {@code List -> Author} will register a {@link io.cucumber.datatable.TableRowTransformer}
  • 19 | *
  • {@code DataTable -> Author} will register a {@link io.cucumber.datatable.TableTransformer}
  • 20 | *
21 | * NOTE: {@code Author} is an example of the class you want to convert the table to. 22 | * 23 | * @see io.cucumber.datatable.DataTableType 24 | */ 25 | @Retention(RetentionPolicy.RUNTIME) 26 | @Target(ElementType.METHOD) 27 | @API(status = API.Status.STABLE) 28 | public @interface DataTableType { 29 | 30 | /** 31 | * Replace these strings in the Datatable with empty strings. 32 | *

33 | * A data table can only represent absent and non-empty strings. By replacing 34 | * a known value (for example [empty]) a data table can also represent 35 | * empty strings. 36 | *

37 | * It is not recommended to use multiple replacements in the same table. 38 | * 39 | * @return strings to be replaced with empty strings. 40 | */ 41 | String[] replaceWithEmptyString() default {}; 42 | } 43 | -------------------------------------------------------------------------------- /groovy/src/test/groovy/io/cucumber/groovy/compiled_stepdefs.groovy: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy 2 | 3 | import io.cucumber.groovy.EN 4 | import io.cucumber.datatable.DataTable 5 | 6 | import static groovy.test.GroovyTestCase.assertEquals 7 | 8 | this.metaClass.mixin(EN) 9 | this.metaClass.mixin(Hooks) 10 | 11 | World { 12 | new CustomWorld() 13 | } 14 | 15 | Given(~'^I have (\\d+) apples in my belly') { int apples -> 16 | lastAte('apples') 17 | } 18 | 19 | Given(~'^a big basket with cukes') { -> 20 | } 21 | 22 | @DataTableType 23 | Thing dataTableTypeThing(Map tableEntry) { 24 | Thing thing = new Thing() 25 | thing.year = Integer.valueOf(tableEntry.get("year")) 26 | thing.name = tableEntry.get("name") 27 | thing 28 | } 29 | 30 | Given(~'^the following table:$') { DataTable table -> 31 | things = table.asList(Thing) 32 | assertEquals("Cucumber-JVM", things[1].name) 33 | } 34 | 35 | @ParameterType(name="list", value="(.+\\s*,\\s*.+){1,}") 36 | List parameterTypeList(String[] s) { 37 | Arrays.asList(s[0].split(","))*.trim() 38 | } 39 | 40 | Given('this should be converted to a list:{list}') { List list -> 41 | assertEquals(3, list.size()) 42 | assertEquals("Cucumber-JVM", list.get(0)) 43 | assertEquals("Cucumber", list.get(1)) 44 | } 45 | 46 | class Thing { 47 | Integer year 48 | String name 49 | } 50 | 51 | Then(~'^there are (\\d+) cukes in my belly') { int cukes -> 52 | checkCukes(cukes) 53 | } 54 | 55 | Then(~'^the (.*) contains (.*)') { String container, String ingredient -> 56 | assertEquals("glass", container) 57 | } 58 | 59 | Then(~'^I add (.*)') { String liquid -> 60 | assertEquals("milk", liquid) 61 | } 62 | 63 | Then(~'^I should be (.*)') { String mood -> 64 | assertEquals(mood, getMood()) 65 | } 66 | -------------------------------------------------------------------------------- /groovy/src/test/java/io/cucumber/groovy/GroovyStackTraceTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import groovy.lang.Closure; 4 | import org.codehaus.groovy.runtime.MethodClosure; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.fail; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | @ExtendWith(MockitoExtension.class) 15 | public class GroovyStackTraceTest { 16 | GroovyStepDefinition groovyStepDefinition; 17 | 18 | 19 | @BeforeEach 20 | public void setUp() { 21 | Closure body = new MethodClosure("the owner", "length"); 22 | groovyStepDefinition = new GroovyStepDefinition("", body, null, new ExceptionThrowingBackend()); 23 | } 24 | 25 | @Test 26 | public void should_sanitize_stacktrace() { 27 | try { 28 | groovyStepDefinition.execute(new Object[0]); 29 | fail("step definition didn't throw an exception"); 30 | } catch (Throwable thrown) { 31 | for (StackTraceElement stackTraceElement : thrown.getStackTrace()) { 32 | // if there are none of these, pretty good chance it's cleaned up the stack trace 33 | assertFalse(stackTraceElement.getClassName().startsWith("org.codehaus.groovy.runtime.callsite"), "Stack trace has internal groovy callsite elements"); 34 | } 35 | } 36 | 37 | } 38 | 39 | private static class ExceptionThrowingBackend extends GroovyBackend { 40 | 41 | 42 | public ExceptionThrowingBackend() { 43 | super(null, null, null); 44 | } 45 | 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /groovy/src/test/java/io/cucumber/groovy/HooksTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | 4 | import io.cucumber.core.exception.CucumberException; 5 | import org.codehaus.groovy.runtime.MethodClosure; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.fail; 10 | 11 | public class HooksTest { 12 | 13 | @Test 14 | public void only_allows_arguments_string_integer_closure() { 15 | try { 16 | Hooks.Before("TAG", 10D, 100, new MethodClosure(this, "dummyClosureCall"), 0.0); 17 | fail("CucumberException was not thrown"); 18 | } catch (CucumberException e) { 19 | assertEquals("An argument of the type java.lang.Double found, Before only allows the argument types " + 20 | "String - Tag, Integer - order, and Closure", 21 | e.getMessage()); 22 | } 23 | } 24 | 25 | @Test 26 | public void only_allows_one_timeout_argument() { 27 | try { 28 | Hooks.Before(1L, 2L); 29 | fail("CucumberException was not thrown"); 30 | } catch (CucumberException e) { 31 | assertEquals("An argument of the type java.lang.Long found, Before only allows the argument types String - Tag, Integer - order, and Closure", e.getMessage()); 32 | } 33 | } 34 | 35 | @Test 36 | public void only_allows_one_order_argument() { 37 | try { 38 | Hooks.Before(1, 2); 39 | fail("CucumberException was not thrown"); 40 | } catch (CucumberException e) { 41 | assertEquals("Two order (Integer) arguments found; 1, and; 2", e.getMessage()); 42 | } 43 | } 44 | 45 | @SuppressWarnings("unused") 46 | private void dummyClosureCall() { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/DefaultDataTableEntryTransformer.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Register default data table entry transformer. 12 | *

13 | * Valid method signatures are: 14 | *

    15 | *
  • {@code Map, Type -> Object}
  • 16 | *
  • {@code Object, Type -> Object}
  • 17 | *
  • {@code Map, Type, TableCellByTypeTransformer -> Object}
  • 18 | *
  • {@code Object, Type, TableCellByTypeTransformer -> Object}
  • 19 | *
20 | * 21 | * @see io.cucumber.datatable.TableEntryByTypeTransformer 22 | * @see io.cucumber.datatable.DataTableType 23 | */ 24 | @Retention(RetentionPolicy.RUNTIME) 25 | @Target(ElementType.METHOD) 26 | @API(status = API.Status.STABLE) 27 | public @interface DefaultDataTableEntryTransformer { 28 | /** 29 | * Converts a data tables header headers to property names. 30 | *

31 | * E.g. {@code Xml Http request} becomes {@code xmlHttpRequest}. 32 | * 33 | * @return true if conversion should be be applied, true by default. 34 | */ 35 | boolean headersToProperties() default true; 36 | 37 | /** 38 | * Replace these strings in the Datatable with empty strings. 39 | *

40 | * A data table can only represent absent and non-empty strings. By replacing 41 | * a known value (for example [empty]) a data table can also represent 42 | * empty strings. 43 | *

44 | * It is not recommended to use multiple replacements in the same table. 45 | * 46 | * @return strings to be replaced with empty strings. 47 | */ 48 | String[] replaceWithEmptyString() default {}; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /groovy/src/test/java/io/cucumber/groovy/GroovyWorldTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | 12 | public class GroovyWorldTest { 13 | GroovyWorld world; 14 | 15 | @BeforeEach 16 | public void setUp() { 17 | world = new GroovyWorld(); 18 | } 19 | 20 | @Test 21 | public void should_not_register_pure_java_object() { 22 | assertThrows(RuntimeException.class, () -> world.registerWorld("JAVA")); 23 | } 24 | 25 | @Test 26 | public void should_support_more_then_one_World() { 27 | world.registerWorld(new CustomWorld()); 28 | world.registerWorld(new AnotherCustomWorld()); 29 | 30 | world.setProperty("lastAte", "groovy"); 31 | assertEquals("groovy", world.getProperty("lastAte")); 32 | 33 | world.setProperty("aProperty", 1); 34 | assertEquals(1, world.getProperty("aProperty")); 35 | 36 | List intArgs = Arrays.asList(1, 2); 37 | world.invokeMethod("aMethod", intArgs); 38 | assertEquals(intArgs, world.getProperty("methodArgs")); 39 | 40 | world.invokeMethod("aMethod", null); 41 | assertEquals("no args", world.getProperty("methodArgs")); 42 | } 43 | 44 | @Test 45 | public void should_detect_double_property_definition() { 46 | world.registerWorld(new WorldWithPropertyAndMethod()); 47 | world.registerWorld(new AnotherCustomWorld()); 48 | 49 | assertThrows(RuntimeException.class, () -> world.getProperty("aProperty")); 50 | } 51 | 52 | @Test 53 | public void should_detect_double_method_definition() { 54 | world.registerWorld(new WorldWithPropertyAndMethod()); 55 | world.registerWorld(new AnotherCustomWorld()); 56 | 57 | assertThrows(RuntimeException.class, () -> world.invokeMethod("aMethod", new Integer[]{1, 2})); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /groovy/src/test/java/io/cucumber/groovy/GroovyBackendTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | 4 | import io.cucumber.core.backend.Container; 5 | import io.cucumber.core.backend.Lookup; 6 | import org.codehaus.groovy.runtime.MethodClosure; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.function.Supplier; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertNull; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | public class GroovyBackendTest { 20 | @Mock 21 | Lookup lookup; 22 | 23 | @Mock 24 | Container container; 25 | 26 | @Mock 27 | GroovyBackend backend; 28 | 29 | @Mock 30 | Supplier classLoaderSupplier; 31 | 32 | @BeforeEach 33 | public void setUp() { 34 | backend = new GroovyBackend(lookup, container, classLoaderSupplier); 35 | } 36 | 37 | @Test 38 | public void should_build_world_by_calling_the_closure() { 39 | backend.registerWorld(new MethodClosure(this, "worldClosureCall")); 40 | backend.buildWorld(); 41 | 42 | GroovyWorld groovyWorld = backend.getGroovyWorld(); 43 | assertEquals(1, groovyWorld.worldsCount()); 44 | } 45 | 46 | @Test 47 | public void should_build_world_object_even_if_closure_world_was_not_added() { 48 | assertNull(backend.getGroovyWorld()); 49 | 50 | backend.buildWorld(); 51 | 52 | assertEquals(0, backend.getGroovyWorld().worldsCount()); 53 | } 54 | 55 | @Test 56 | public void should_clean_up_worlds_after_dispose() { 57 | backend.registerWorld(new MethodClosure(this, "worldClosureCall")); 58 | backend.buildWorld(); 59 | 60 | backend.disposeWorld(); 61 | 62 | assertNull(backend.getGroovyWorld()); 63 | } 64 | 65 | @SuppressWarnings("UnusedDeclaration") 66 | private AnotherCustomWorld worldClosureCall() { 67 | return new AnotherCustomWorld(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | Hooks are blocks of code that can run at various points in the Cucumber execution cycle. 4 | They are typically used for setup and teardown of the environment before and after each scenario. 5 | 6 | See the [reference documentation](https://docs.cucumber.io/docs/cucumber/api/#hooks). 7 | 8 | ## Scenario hooks 9 | 10 | Scenario hooks run for every scenario. 11 | 12 | ### Before 13 | 14 | `Before` hooks run before the first step of each scenario. 15 | 16 | ```groovy 17 | Before() { scenario -> 18 | // Do something before each scenario 19 | } 20 | 21 | // Or: 22 | Before() { 23 | // Do something before each scenario 24 | } 25 | ``` 26 | 27 | ### After 28 | 29 | `After` hooks run after the last step of each scenario. 30 | 31 | ```groovy 32 | After() { scenario -> 33 | // Do something after each scenario 34 | } 35 | 36 | // Or: 37 | After() { 38 | // Do something after each scenario 39 | } 40 | ``` 41 | 42 | ## Step hooks 43 | 44 | Step hooks invoked before and after a step. 45 | 46 | ### BeforeStep 47 | 48 | ```groovy 49 | BeforeStep() { scenario -> 50 | // Do something before step 51 | } 52 | 53 | // Or: 54 | BeforeStep() { 55 | // Do something before step 56 | } 57 | ``` 58 | 59 | ### AfterStep 60 | 61 | ```groovy 62 | AfterStep() { scenario -> 63 | // Do something after step 64 | } 65 | 66 | // Or: 67 | AfterStep() { 68 | // Do something after step 69 | } 70 | ``` 71 | 72 | ## Conditional hooks 73 | 74 | Hooks can be conditionally selected for execution based on the tags of the scenario. 75 | 76 | ```groovy 77 | Before("@browser and not @headless") { 78 | // Do something before each scenario with tag @browser but not @headless 79 | } 80 | ``` 81 | 82 | ## Order 83 | 84 | You can define an order between multiple hooks. 85 | 86 | ```groovy 87 | Before(10) { 88 | // Do something before each scenario 89 | } 90 | 91 | Before(20) { 92 | // Do something before each scenario 93 | } 94 | ``` 95 | 96 | The **default order is 1000**. 97 | 98 | ## Conditional and order 99 | 100 | You mix up conditional and order hooks with following syntax: 101 | 102 | ```groovy 103 | Before("@browser and not @headless", 10) { 104 | // Do something before each scenario 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/GroovySnippet.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.backend.Snippet; 4 | import io.cucumber.datatable.DataTable; 5 | 6 | import java.lang.reflect.Type; 7 | import java.text.MessageFormat; 8 | import java.util.Map; 9 | 10 | public class GroovySnippet implements Snippet { 11 | 12 | @Override 13 | public final String arguments(Map arguments) { 14 | StringBuilder sb = new StringBuilder(); 15 | boolean first = true; 16 | for (Map.Entry argType : arguments.entrySet()) { 17 | if (first) { 18 | first = false; 19 | } else { 20 | sb.append(", "); 21 | } 22 | sb.append(getArgType(argType.getValue())).append(" ").append(argType.getKey()); 23 | } 24 | return sb.toString(); 25 | } 26 | 27 | private String getArgType(Type argType) { 28 | if (argType instanceof Class) { 29 | Class cType = (Class) argType; 30 | if (cType.equals(DataTable.class)) { 31 | return cType.getName(); 32 | } 33 | return cType.getSimpleName(); 34 | } 35 | 36 | // Got a better idea? Send a PR. 37 | return argType.toString(); 38 | } 39 | 40 | @Override 41 | public MessageFormat template() { 42 | return new MessageFormat("{0}(/{1}/) '{' {3} ->\n" + 43 | " // {4}\n" + 44 | " throw new " + PendingException.class.getName() + "()\n" + 45 | "'}'\n"); 46 | } 47 | 48 | @Override 49 | public final String tableHint() { 50 | return "" + 51 | " // For automatic transformation, change DataTable to one of\n" + 52 | " // E, List, List>, List>, Map or\n" + 53 | " // Map>. E,K,V must be a String, Integer, Float,\n" + 54 | " // Double, Byte, Short, Long, BigInteger or BigDecimal.\n" + 55 | " //\n" + 56 | " // For other transformations you can register a DataTableType.\n"; //TODO: Add doc URL 57 | } 58 | 59 | @Override 60 | public final String escapePattern(String pattern) { 61 | return pattern.replace("\\", "\\\\").replace("\"", "\\\""); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/src/test/groovy/calc/CalculatorSteps.groovy: -------------------------------------------------------------------------------- 1 | package calc 2 | 3 | import io.cucumber.groovy.EN 4 | import io.cucumber.groovy.Hooks 5 | 6 | // Add functions to register hooks and steps to this script. 7 | this.metaClass.mixin(Hooks) 8 | this.metaClass.mixin(EN) 9 | 10 | // Define a world that represents the test environment. 11 | // Hooks can set up and tear down the environment and steps 12 | // can change its state, e.g. store values used by later steps. 13 | class CustomWorld { 14 | def result 15 | 16 | String customMethod() { 17 | "foo" 18 | } 19 | } 20 | 21 | // Create a fresh new world object as the test environment for each scenario. 22 | // Hooks and steps will belong to this object so can access its properties 23 | // and methods directly. 24 | World { 25 | new CustomWorld() 26 | } 27 | 28 | // This closure gets run before each scenario 29 | // and has direct access to the new world object 30 | // but can also make use of script variables. 31 | Before() { 32 | assert "foo" == customMethod() 33 | calc = new Calculator() // belongs to this script 34 | } 35 | 36 | // Register another that also gets run before each scenario tagged with @notused. 37 | Before("@notused") { 38 | throw new RuntimeException("Never happens") 39 | } 40 | 41 | // Register another that also gets run before each scenario tagged with 42 | // (@notused or @important) and @alsonotused. 43 | Before("(@notused or @important) and @alsonotused") { 44 | throw new RuntimeException("Never happens") 45 | } 46 | 47 | // Register step definition using Groovy syntax for regex patterns. 48 | // If you use slashes to quote your regexes, you don't have to escape backslashes. 49 | // Any Given/When/Then function can be used, the name is just to indicate the kind of step. 50 | Given(~/^I have entered (\d+) into .* calculator$/) { int number -> 51 | calc.push number 52 | } 53 | 54 | // This step calls a Calculator function specified in the step 55 | // and saves the result in the current world object. 56 | When(~/^I press (\w+)$/) { String opname -> 57 | result = calc."$opname"() 58 | } 59 | 60 | // Use the world object to get any result from a previous step. 61 | // The expected value in the step is converted to the required type. 62 | Then(/the stored result should be {}/) { double expected -> 63 | assert expected == result 64 | } 65 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/GroovyDocStringTypeDefinition.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.backend.DocStringTypeDefinition; 4 | import io.cucumber.core.backend.Lookup; 5 | import io.cucumber.docstring.DocStringType; 6 | 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Type; 9 | 10 | import static io.cucumber.groovy.InvalidMethodSignatureException.builder; 11 | 12 | class GroovyDocStringTypeDefinition extends AbstractParamGlueDefinition implements DocStringTypeDefinition { 13 | 14 | private final io.cucumber.docstring.DocStringType docStringType; 15 | 16 | 17 | GroovyDocStringTypeDefinition(String contentType, Method method, Lookup lookup) { 18 | super(requireValidMethod(method), lookup); 19 | this.docStringType = new DocStringType( 20 | this.method.getReturnType(), 21 | contentType.isEmpty() ? method.getName() : contentType, 22 | this::execute 23 | ); 24 | } 25 | 26 | private static Method requireValidMethod(Method method) { 27 | Type returnType = method.getGenericReturnType(); 28 | if (Void.class.equals(returnType) || void.class.equals(returnType)) { 29 | throw createInvalidSignatureException(method); 30 | } 31 | 32 | Type[] parameterTypes = method.getGenericParameterTypes(); 33 | if (parameterTypes.length != 1) { 34 | throw createInvalidSignatureException(method); 35 | } 36 | 37 | for (Type parameterType : parameterTypes) { 38 | if (!String.class.equals(parameterType)) { 39 | throw createInvalidSignatureException(method); 40 | } 41 | } 42 | 43 | return method; 44 | } 45 | 46 | private static InvalidMethodSignatureException createInvalidSignatureException(Method method) { 47 | return builder(method) 48 | .addAnnotation(io.cucumber.groovy.DocStringType.class) 49 | .addSignature("JsonNode json(String content)") 50 | .addNote("Note: JsonNode is an example of the class you want to convert content to") 51 | .build(); 52 | } 53 | 54 | 55 | private Object execute(String content) { 56 | return Invoker.invoke(this, lookup.getInstance(method.getDeclaringClass()), method, content); 57 | } 58 | 59 | @Override 60 | public DocStringType docStringType() { 61 | return docStringType; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/GroovyDefaultParameterTransformerDefinition.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.backend.DefaultParameterTransformerDefinition; 4 | import io.cucumber.core.backend.Lookup; 5 | import io.cucumber.cucumberexpressions.ParameterByTypeTransformer; 6 | 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Type; 9 | 10 | import static io.cucumber.groovy.InvalidMethodSignatureException.builder; 11 | 12 | class GroovyDefaultParameterTransformerDefinition extends AbstractParamGlueDefinition implements DefaultParameterTransformerDefinition { 13 | 14 | private final Lookup lookup; 15 | private final ParameterByTypeTransformer transformer; 16 | 17 | GroovyDefaultParameterTransformerDefinition(Method method, Lookup lookup) { 18 | super(requireValidMethod(method), lookup); 19 | this.lookup = lookup; 20 | this.transformer = this::execute; 21 | } 22 | 23 | private static Method requireValidMethod(Method method) { 24 | Class returnType = method.getReturnType(); 25 | if (Void.class.equals(returnType) || void.class.equals(returnType)) { 26 | throw createInvalidSignatureException(method); 27 | } 28 | 29 | Class[] parameterTypes = method.getParameterTypes(); 30 | if (parameterTypes.length != 2) { 31 | throw createInvalidSignatureException(method); 32 | } 33 | 34 | if (!(Object.class.equals(parameterTypes[0]) || String.class.equals(parameterTypes[0]))) { 35 | throw createInvalidSignatureException(method); 36 | } 37 | 38 | if (!Type.class.equals(parameterTypes[1])) { 39 | throw createInvalidSignatureException(method); 40 | } 41 | 42 | return method; 43 | } 44 | 45 | private static InvalidMethodSignatureException createInvalidSignatureException(Method method) { 46 | return builder(method) 47 | .addAnnotation(DefaultParameterTransformer.class) 48 | .addSignature("Object defaultDataTableEntry(String fromValue, Type toValueType)") 49 | .addSignature("Object defaultDataTableEntry(Object fromValue, Type toValueType)") 50 | .build(); 51 | } 52 | 53 | 54 | @Override 55 | public ParameterByTypeTransformer parameterByTypeTransformer() { 56 | return transformer; 57 | } 58 | 59 | private Object execute(String fromValue, Type toValueType) { 60 | return Invoker.invoke(this, lookup.getInstance(method.getDeclaringClass()), method, fromValue, toValueType); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/MethodFormat.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import io.cucumber.core.backend.CucumberBackendException; 4 | 5 | import java.lang.reflect.Method; 6 | import java.text.MessageFormat; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | /** 11 | * Helper class for formatting a method signature to a shorter form. 12 | */ 13 | final class MethodFormat { 14 | static final MethodFormat FULL = new MethodFormat("%qc.%m(%qa)"); 15 | private static final Pattern METHOD_PATTERN = Pattern.compile("((?:static\\s|public\\s)+)([^\\s]*)\\s\\.?(.*)\\.([^\\(]*)\\(([^\\)]*)\\)(?: throws )?(.*)"); 16 | private final MessageFormat format; 17 | 18 | /** 19 | * @param format the format string to use. There are several pattern tokens that can be used: 20 | *

    21 | *
  • %M: Modifiers
  • 22 | *
  • %qr: Qualified return type
  • 23 | *
  • %r: Unqualified return type
  • 24 | *
  • %qc: Qualified class
  • 25 | *
  • %c: Unqualified class
  • 26 | *
  • %m: Method name
  • 27 | *
  • %qa: Qualified arguments
  • 28 | *
  • %a: Unqualified arguments
  • 29 | *
  • %qe: Qualified exceptions
  • 30 | *
  • %e: Unqualified exceptions
  • 31 | *
  • %s: Code source
  • 32 | *
33 | */ 34 | private MethodFormat(String format) { 35 | String pattern = format 36 | .replaceAll("%qc", "{0}") 37 | .replaceAll("%m", "{1}") 38 | .replaceAll("%qa", "{2}"); 39 | this.format = new MessageFormat(pattern); 40 | } 41 | 42 | String format(Method method) { 43 | String signature = method.toGenericString(); 44 | Matcher matcher = METHOD_PATTERN.matcher(signature); 45 | if (matcher.find()) { 46 | String qc = matcher.group(3); 47 | String m = matcher.group(4); 48 | String qa = matcher.group(5); 49 | 50 | return format.format(new Object[]{ 51 | qc, 52 | m, 53 | qa, 54 | }); 55 | } else { 56 | throw new CucumberBackendException("Cucumber bug: Couldn't format " + signature); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /groovy/src/main/java/io/cucumber/groovy/GroovyScriptIdentifier.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.groovy; 2 | 3 | import groovy.lang.GroovyShell; 4 | import groovy.lang.Script; 5 | import io.cucumber.core.resource.Resource; 6 | import org.codehaus.groovy.runtime.DefaultGroovyMethods; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStreamReader; 10 | import java.net.URI; 11 | import java.nio.charset.StandardCharsets; 12 | import java.nio.file.Path; 13 | import java.util.Optional; 14 | 15 | public class GroovyScriptIdentifier { 16 | 17 | private static final String GROOVY_FILE_SUFFIX = ".groovy"; 18 | 19 | private GroovyScriptIdentifier() { 20 | } 21 | 22 | public static URI parse(String groovyIdentifier) { 23 | return parse(ScriptPath.parse(groovyIdentifier)); 24 | } 25 | 26 | public static URI parse(URI groovyIdentifier) { 27 | if (!isGroovyScript(groovyIdentifier)) { 28 | throw new IllegalArgumentException("Groovy identifier does not reference a single groovy file: " + groovyIdentifier); 29 | } else { 30 | return groovyIdentifier; 31 | } 32 | } 33 | 34 | public static boolean isGroovyScript(URI groovyIdentifier) { 35 | return groovyIdentifier.getSchemeSpecificPart().endsWith(GROOVY_FILE_SUFFIX); 36 | } 37 | 38 | public static boolean isGroovyScript(Path path) { 39 | return path.getFileName().toString().endsWith(GROOVY_FILE_SUFFIX); 40 | } 41 | 42 | public static StackTraceElement currentLocation() { 43 | Throwable t = new Throwable(); 44 | StackTraceElement[] stackTraceElements = t.getStackTrace(); 45 | for (StackTraceElement stackTraceElement : stackTraceElements) { 46 | if (isGroovyFile(stackTraceElement.getFileName())) { 47 | return stackTraceElement; 48 | } 49 | } 50 | throw new RuntimeException("Couldn't find location for step definition"); 51 | } 52 | 53 | public static boolean isGroovyFile(String fileName) { 54 | return fileName != null && fileName.endsWith(".groovy"); 55 | } 56 | 57 | public static boolean isScript(Script script) { 58 | return DefaultGroovyMethods.asBoolean(script.getMetaClass().respondsTo(script, "main")); 59 | } 60 | 61 | public static Optional