diff --git a/CHANGELOG.md b/CHANGELOG.md index 04399372..e8cac414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Check out the [Upgrade Guide](docs/upgrade_v8.md). ### Added +- [Scala] Added `BeforeAll` and `AfterAll` hooks. See [Hooks](docs/hooks.md). + ### Changed - [Core] Updated `cucumber-core` dependency to [7.0.0-RC1](https://github.com/cucumber/cucumber-jvm/blob/main/CHANGELOG.md) diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/Aliases.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/Aliases.scala index e35d5019..ec328137 100644 --- a/cucumber-scala/src/main/scala/io/cucumber/scala/Aliases.scala +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/Aliases.scala @@ -4,6 +4,8 @@ package io.cucumber.scala */ object Aliases { + type StaticHookDefinitionBody = () => Unit + type HookDefinitionBody = Scenario => Unit type StepDefinitionBody = () => Unit diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/GlueAdaptor.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/GlueAdaptor.scala index 7fbd3207..3c409881 100644 --- a/cucumber-scala/src/main/scala/io/cucumber/scala/GlueAdaptor.scala +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/GlueAdaptor.scala @@ -15,7 +15,7 @@ class GlueAdaptor(glue: Glue) { ): Unit = { // If the registry is not consistent, this indicates a mistake in the users definition and we want to let him know. - registry.checkConsistency().left.foreach { + registry.checkConsistency(scenarioScoped).left.foreach { (ex: IncorrectHookDefinitionException) => throw ex } @@ -23,6 +23,17 @@ class GlueAdaptor(glue: Glue) { registry.stepDefinitions .map(ScalaStepDefinition(_, scenarioScoped)) .foreach(glue.addStepDefinition) + + // The presence of beforeAll/afterAll hooks with scenarioScoped is checked by checkConsistency above + if (!scenarioScoped) { + registry.beforeAllHooks + .map(ScalaStaticHookDefinition(_)) + .foreach(glue.addBeforeAllHook) + registry.afterAllHooks + .map(ScalaStaticHookDefinition(_)) + .foreach(glue.addAfterAllHook) + } + registry.beforeHooks .map(ScalaHookDefinition(_, scenarioScoped)) .foreach(glue.addBeforeHook) @@ -35,6 +46,7 @@ class GlueAdaptor(glue: Glue) { registry.afterStepHooks .map(ScalaHookDefinition(_, scenarioScoped)) .foreach(glue.addAfterStepHook) + registry.docStringTypes .map(ScalaDocStringTypeDefinition(_, scenarioScoped)) .foreach(glue.addDocStringType) @@ -44,6 +56,7 @@ class GlueAdaptor(glue: Glue) { registry.parameterTypes .map(ScalaParameterTypeDefinition(_, scenarioScoped)) .foreach(glue.addParameterType) + registry.defaultParameterTransformers .map(ScalaDefaultParameterTransformerDefinition(_, scenarioScoped)) .foreach(glue.addDefaultParameterTransformer) diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/HookDsl.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/HookDsl.scala index 6596a8e3..e71a879f 100644 --- a/cucumber-scala/src/main/scala/io/cucumber/scala/HookDsl.scala +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/HookDsl.scala @@ -3,6 +3,19 @@ package io.cucumber.scala private[scala] trait HookDsl extends BaseScalaDsl { self => + /** Defines a before all hook. + */ + def BeforeAll: StaticHookBody = BeforeAll(DEFAULT_AFTER_ORDER) + + /** Defines a before all hook. + * @param order the order in which this hook should run. Higher numbers are run first + */ + def BeforeAll(order: Int): StaticHookBody = new StaticHookBody( + StaticHookType.BEFORE_ALL, + order, + Utils.frame(self) + ) + /** Defines an before hook. */ def Before: HookBody = Before(EMPTY_TAG_EXPRESSION, DEFAULT_BEFORE_ORDER) @@ -26,7 +39,12 @@ private[scala] trait HookDsl extends BaseScalaDsl { * @param order the order in which this hook should run. Higher numbers are run first */ def Before(tagExpression: String, order: Int) = - new HookBody(HookType.BEFORE, tagExpression, order, Utils.frame(self)) + new HookBody( + ScopedHookType.BEFORE, + tagExpression, + order, + Utils.frame(self) + ) /** Defines an before step hook. */ @@ -52,7 +70,22 @@ private[scala] trait HookDsl extends BaseScalaDsl { * @param order the order in which this hook should run. Higher numbers are run first */ def BeforeStep(tagExpression: String, order: Int) = - new HookBody(HookType.BEFORE_STEP, tagExpression, order, Utils.frame(self)) + new HookBody( + ScopedHookType.BEFORE_STEP, + tagExpression, + order, + Utils.frame(self) + ) + + /** Defines a after all hook. + */ + def AfterAll: StaticHookBody = AfterAll(DEFAULT_AFTER_ORDER) + + /** Defines a after all hook. + * @param order the order in which this hook should run. Higher numbers are run first + */ + def AfterAll(order: Int): StaticHookBody = + new StaticHookBody(StaticHookType.AFTER_ALL, order, Utils.frame(self)) /** Defines and after hook. */ @@ -77,7 +110,7 @@ private[scala] trait HookDsl extends BaseScalaDsl { * @param order the order in which this hook should run. Higher numbers are run first */ def After(tagExpression: String, order: Int) = - new HookBody(HookType.AFTER, tagExpression, order, Utils.frame(self)) + new HookBody(ScopedHookType.AFTER, tagExpression, order, Utils.frame(self)) /** Defines and after step hook. */ @@ -102,10 +135,15 @@ private[scala] trait HookDsl extends BaseScalaDsl { * @param order the order in which this hook should run. Higher numbers are run first */ def AfterStep(tagExpression: String, order: Int) = - new HookBody(HookType.AFTER_STEP, tagExpression, order, Utils.frame(self)) + new HookBody( + ScopedHookType.AFTER_STEP, + tagExpression, + order, + Utils.frame(self) + ) final class HookBody( - hookType: HookType, + hookType: ScopedHookType, tagExpression: String, order: Int, frame: StackTraceElement @@ -120,8 +158,25 @@ private[scala] trait HookDsl extends BaseScalaDsl { } def apply(body: Scenario => Unit): Unit = { - val details = ScalaHookDetails(tagExpression, order, body) - registry.registerHook(hookType, details, frame) + val details = ScalaHookDetails(tagExpression, order, body, frame) + registry.registerDynamicHook(hookType, details) + } + + } + + final class StaticHookBody( + hookType: StaticHookType, + order: Int, + frame: StackTraceElement + ) { + + // When a HookBody is created, we want to ensure that the apply method is called + // To be able to check this, we notice the registry to expect a hook + registry.expectHook(hookType, frame) + + def apply(body: => Unit): Unit = { + val details = ScalaStaticHookDetails(order, () => body, frame) + registry.registerStaticHook(hookType, details) } } diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/HookType.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/HookType.scala index b4a9afda..a470c738 100644 --- a/cucumber-scala/src/main/scala/io/cucumber/scala/HookType.scala +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/HookType.scala @@ -2,14 +2,26 @@ package io.cucumber.scala sealed trait HookType -object HookType { +sealed trait ScopedHookType extends HookType - case object BEFORE extends HookType +object ScopedHookType { - case object BEFORE_STEP extends HookType + case object BEFORE extends ScopedHookType - case object AFTER extends HookType + case object BEFORE_STEP extends ScopedHookType - case object AFTER_STEP extends HookType + case object AFTER extends ScopedHookType + + case object AFTER_STEP extends ScopedHookType + +} + +sealed trait StaticHookType extends HookType + +object StaticHookType { + + case object BEFORE_ALL extends StaticHookType + + case object AFTER_ALL extends StaticHookType } diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/IncorrectHookDefinitionException.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/IncorrectHookDefinitionException.scala index 2d7aaa29..7f774c7c 100644 --- a/cucumber-scala/src/main/scala/io/cucumber/scala/IncorrectHookDefinitionException.scala +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/IncorrectHookDefinitionException.scala @@ -2,9 +2,12 @@ package io.cucumber.scala import io.cucumber.core.backend.CucumberBackendException +sealed abstract class IncorrectHookDefinitionException(message: String) + extends CucumberBackendException(message) + object IncorrectHookDefinitionException { - def errorMessage(expectedHooks: Seq[UndefinedHook]): String = { + def undefinedHooksErrorMessage(expectedHooks: Seq[UndefinedHook]): String = { val hooksListToDisplay = expectedHooks.map { eh => s" - ${eh.stackTraceElement.getFileName}:${eh.stackTraceElement.getLineNumber} (${eh.hookType})" } @@ -29,11 +32,36 @@ object IncorrectHookDefinitionException { |""".stripMargin } + def scenarioScopedStaticHookErrorMessage( + staticHooks: Seq[ScalaStaticHookDetails] + ): String = { + val hooksListToDisplay: Seq[String] = staticHooks.map { h => + s" - ${h.stackTraceElement.getFileName}:${h.stackTraceElement.getLineNumber}" + } + + s"""Some hooks are not defined properly: + |${hooksListToDisplay.mkString("\n")} + | + |This can be caused by defining static hooks (BeforeAll/AfterAll) in a class rather than in a object. + |Such hooks can only be defined in a static context. + |""".stripMargin + } + } -class IncorrectHookDefinitionException(val undefinedHooks: Seq[UndefinedHook]) - extends CucumberBackendException( - IncorrectHookDefinitionException.errorMessage(undefinedHooks) +class UndefinedHooksException(val undefinedHooks: Seq[UndefinedHook]) + extends IncorrectHookDefinitionException( + IncorrectHookDefinitionException.undefinedHooksErrorMessage( + undefinedHooks + ) + ) {} + +class ScenarioScopedStaticHookException( + val staticHooks: Seq[ScalaStaticHookDetails] +) extends IncorrectHookDefinitionException( + IncorrectHookDefinitionException.scenarioScopedStaticHookErrorMessage( + staticHooks + ) ) {} case class UndefinedHook( diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDslRegistry.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDslRegistry.scala index 3c1a669f..438dafff 100644 --- a/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDslRegistry.scala +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDslRegistry.scala @@ -1,16 +1,23 @@ package io.cucumber.scala -import io.cucumber.scala.HookType.{AFTER, AFTER_STEP, BEFORE, BEFORE_STEP} +import io.cucumber.scala.ScopedHookType._ +import io.cucumber.scala.StaticHookType.{AFTER_ALL, BEFORE_ALL} final class ScalaDslRegistry { private var _stepDefinitions: Seq[ScalaStepDetails] = Seq() + private var _beforeAllHooks: Seq[ScalaStaticHookDetails] = Seq() + private var _afterAllHooks: Seq[ScalaStaticHookDetails] = Seq() + private var _beforeHooks: Seq[ScalaHookDetails] = Seq() private var _beforeStepHooks: Seq[ScalaHookDetails] = Seq() private var _afterHooks: Seq[ScalaHookDetails] = Seq() private var _afterStepHooks: Seq[ScalaHookDetails] = Seq() + private var _undefinedBeforeAllHooks: Seq[UndefinedHook] = Seq() + private var _undefinedAfterAllHooks: Seq[UndefinedHook] = Seq() + private var _undefinedBeforeHooks: Seq[UndefinedHook] = Seq() private var _undefinedBeforeStepHooks: Seq[UndefinedHook] = Seq() private var _undefinedAfterHooks: Seq[UndefinedHook] = Seq() @@ -33,10 +40,14 @@ final class ScalaDslRegistry { def stepDefinitions: Seq[ScalaStepDetails] = _stepDefinitions + def beforeAllHooks: Seq[ScalaStaticHookDetails] = _beforeAllHooks + def beforeHooks: Seq[ScalaHookDetails] = _beforeHooks def beforeStepHooks: Seq[ScalaHookDetails] = _beforeStepHooks + def afterAllHooks: Seq[ScalaStaticHookDetails] = _afterAllHooks + def afterHooks: Seq[ScalaHookDetails] = _afterHooks def afterStepHooks: Seq[ScalaHookDetails] = _afterStepHooks @@ -63,46 +74,66 @@ final class ScalaDslRegistry { hookType: HookType, stackTraceElement: StackTraceElement ): Unit = { + val undefinedHook = UndefinedHook(hookType, stackTraceElement) hookType match { + case BEFORE_ALL => + _undefinedBeforeAllHooks = _undefinedBeforeAllHooks :+ undefinedHook case BEFORE => - _undefinedBeforeHooks = - _undefinedBeforeHooks :+ UndefinedHook(hookType, stackTraceElement) + _undefinedBeforeHooks = _undefinedBeforeHooks :+ undefinedHook case BEFORE_STEP => - _undefinedBeforeStepHooks = _undefinedBeforeStepHooks :+ UndefinedHook( - hookType, - stackTraceElement - ) + _undefinedBeforeStepHooks = _undefinedBeforeStepHooks :+ undefinedHook + case AFTER_ALL => + _undefinedAfterAllHooks = _undefinedAfterAllHooks :+ undefinedHook case AFTER => - _undefinedAfterHooks = - _undefinedAfterHooks :+ UndefinedHook(hookType, stackTraceElement) + _undefinedAfterHooks = _undefinedAfterHooks :+ undefinedHook case AFTER_STEP => - _undefinedAfterStepHooks = - _undefinedAfterStepHooks :+ UndefinedHook(hookType, stackTraceElement) + _undefinedAfterStepHooks = _undefinedAfterStepHooks :+ undefinedHook } } - def registerHook( - hookType: HookType, - details: ScalaHookDetails, - frame: StackTraceElement + def registerDynamicHook( + hookType: ScopedHookType, + details: ScalaHookDetails ): Unit = { hookType match { - case HookType.BEFORE => + case BEFORE => _beforeHooks = _beforeHooks :+ details - _undefinedBeforeHooks = - _undefinedBeforeHooks.filterNot(_.stackTraceElement == frame) - case HookType.BEFORE_STEP => + _undefinedBeforeHooks = _undefinedBeforeHooks.filterNot( + _.stackTraceElement == details.stackTraceElement + ) + case BEFORE_STEP => _beforeStepHooks = _beforeStepHooks :+ details - _undefinedBeforeStepHooks = - _undefinedBeforeStepHooks.filterNot(_.stackTraceElement == frame) - case HookType.AFTER => + _undefinedBeforeStepHooks = _undefinedBeforeStepHooks.filterNot( + _.stackTraceElement == details.stackTraceElement + ) + case AFTER => _afterHooks = _afterHooks :+ details - _undefinedAfterHooks = - _undefinedAfterHooks.filterNot(_.stackTraceElement == frame) - case HookType.AFTER_STEP => + _undefinedAfterHooks = _undefinedAfterHooks.filterNot( + _.stackTraceElement == details.stackTraceElement + ) + case AFTER_STEP => _afterStepHooks = _afterStepHooks :+ details - _undefinedAfterStepHooks = - _undefinedAfterStepHooks.filterNot(_.stackTraceElement == frame) + _undefinedAfterStepHooks = _undefinedAfterStepHooks.filterNot( + _.stackTraceElement == details.stackTraceElement + ) + } + } + + def registerStaticHook( + hookType: StaticHookType, + details: ScalaStaticHookDetails + ): Unit = { + hookType match { + case BEFORE_ALL => + _beforeAllHooks = _beforeAllHooks :+ details + _undefinedBeforeAllHooks = _undefinedBeforeAllHooks.filterNot( + _.stackTraceElement == details.stackTraceElement + ) + case AFTER_ALL => + _afterAllHooks = _afterAllHooks :+ details + _undefinedAfterAllHooks = _undefinedAfterAllHooks.filterNot( + _.stackTraceElement == details.stackTraceElement + ) } } @@ -142,11 +173,16 @@ final class ScalaDslRegistry { _defaultParameterTransformers = _defaultParameterTransformers :+ details } - def checkConsistency(): Either[IncorrectHookDefinitionException, Unit] = { + def checkConsistency( + scenarioScoped: Boolean + ): Either[IncorrectHookDefinitionException, Unit] = { val undefinedHooks = - _undefinedBeforeHooks ++ _undefinedBeforeStepHooks ++ _undefinedAfterHooks ++ _undefinedAfterStepHooks - if (undefinedHooks.nonEmpty) { - Left(new IncorrectHookDefinitionException(undefinedHooks)) + _undefinedBeforeAllHooks ++ _undefinedBeforeHooks ++ _undefinedBeforeStepHooks ++ _undefinedAfterAllHooks ++ _undefinedAfterHooks ++ _undefinedAfterStepHooks + val staticHooks = _beforeAllHooks ++ _afterAllHooks + if (scenarioScoped && staticHooks.nonEmpty) { + Left(new ScenarioScopedStaticHookException(staticHooks)) + } else if (undefinedHooks.nonEmpty) { + Left(new UndefinedHooksException(undefinedHooks)) } else { Right(()) } diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaHookDetails.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaHookDetails.scala index 6da77cef..de368de5 100644 --- a/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaHookDetails.scala +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaHookDetails.scala @@ -5,5 +5,6 @@ import Aliases.HookDefinitionBody case class ScalaHookDetails( tagExpression: String, order: Int, - body: HookDefinitionBody + body: HookDefinitionBody, + stackTraceElement: StackTraceElement ) diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDefinition.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDefinition.scala new file mode 100644 index 00000000..68250082 --- /dev/null +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDefinition.scala @@ -0,0 +1,33 @@ +package io.cucumber.scala + +import io.cucumber.core.backend.StaticHookDefinition + +trait ScalaStaticHookDefinition + extends StaticHookDefinition + with AbstractGlueDefinition { + + val hookDetails: ScalaStaticHookDetails + + override val location: StackTraceElement = new Exception().getStackTrace()(3) + + override def execute(): Unit = { + executeAsCucumber(hookDetails.body.apply()) + } + + override def getOrder: Int = hookDetails.order + +} + +object ScalaStaticHookDefinition { + + def apply( + scalaHookDetails: ScalaStaticHookDetails + ): ScalaStaticHookDefinition = { + new ScalaGlobalStaticHookDefinition(scalaHookDetails) + } + +} + +class ScalaGlobalStaticHookDefinition( + override val hookDetails: ScalaStaticHookDetails +) extends ScalaStaticHookDefinition {} diff --git a/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDetails.scala b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDetails.scala new file mode 100644 index 00000000..9b14c497 --- /dev/null +++ b/cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDetails.scala @@ -0,0 +1,9 @@ +package io.cucumber.scala + +import io.cucumber.scala.Aliases.StaticHookDefinitionBody + +case class ScalaStaticHookDetails( + order: Int, + body: StaticHookDefinitionBody, + stackTraceElement: StackTraceElement +) diff --git a/cucumber-scala/src/test/resources/tests/statichooks/statichooks.feature b/cucumber-scala/src/test/resources/tests/statichooks/statichooks.feature new file mode 100644 index 00000000..e2dbd70a --- /dev/null +++ b/cucumber-scala/src/test/resources/tests/statichooks/statichooks.feature @@ -0,0 +1,15 @@ +Feature: As Cucumber Scala, I want to use beforeAll/afterAll hooks + + Scenario: Scenario A + Then BeforeAll count is 1 + Then AfterAll count is 0 + When I run scenario "A" + Then BeforeAll count is 1 + Then AfterAll count is 0 + + Scenario: Scenario B + Then BeforeAll count is 1 + Then AfterAll count is 0 + When I run scenario "B" + Then BeforeAll count is 1 + Then AfterAll count is 0 diff --git a/cucumber-scala/src/test/resources/tests/statichooks/statichooks2.feature b/cucumber-scala/src/test/resources/tests/statichooks/statichooks2.feature new file mode 100644 index 00000000..a07e289e --- /dev/null +++ b/cucumber-scala/src/test/resources/tests/statichooks/statichooks2.feature @@ -0,0 +1,15 @@ +Feature: As Cucumber Scala, I want to use beforeAll/afterAll hooks + + Scenario: Scenario C + Then BeforeAll count is 1 + Then AfterAll count is 0 + When I run scenario "C" + Then BeforeAll count is 1 + Then AfterAll count is 0 + + Scenario: Scenario D + Then BeforeAll count is 1 + Then AfterAll count is 0 + When I run scenario "D" + Then BeforeAll count is 1 + Then AfterAll count is 0 diff --git a/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaBackendTest.scala b/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaBackendTest.scala index 0ce049fd..adfb620e 100644 --- a/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaBackendTest.scala +++ b/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaBackendTest.scala @@ -1,17 +1,17 @@ package io.cucumber.scala -import java.net.URI -import java.util.function.Supplier - import io.cucumber.core.backend._ import io.cucumber.scala.steps.classes.{StepsA, StepsB, StepsC} import io.cucumber.scala.steps.errors.incorrectclasshooks.IncorrectClassHooksDefinition +import io.cucumber.scala.steps.errors.staticclasshooks.StaticClassHooksDefinition import io.cucumber.scala.steps.traits.StepsInTrait import org.junit.Assert.{assertEquals, assertTrue, fail} import org.junit.{Before, Test} import org.mockito.ArgumentMatchers.any import org.mockito.Mockito._ +import java.net.URI +import java.util.function.Supplier import scala.annotation.nowarn import scala.jdk.CollectionConverters._ import scala.util.{Failure, Success, Try} @@ -42,6 +42,8 @@ class ScalaBackendTest { .thenReturn(new StepsInTrait()) when(fakeLookup.getInstance(classOf[IncorrectClassHooksDefinition])) .thenReturn(new IncorrectClassHooksDefinition()) + when(fakeLookup.getInstance(classOf[StaticClassHooksDefinition])) + .thenReturn(new StaticClassHooksDefinition()) // Create the instances backend = new ScalaBackend(fakeLookup, fakeContainer, classLoaderSupplier) @@ -194,15 +196,17 @@ class ScalaBackendTest { } result match { - case Failure(ex) if ex.isInstanceOf[IncorrectHookDefinitionException] => + case Failure(ex) if ex.isInstanceOf[UndefinedHooksException] => val incorrectHookDefException = - ex.asInstanceOf[IncorrectHookDefinitionException] - assertEquals(4, incorrectHookDefException.undefinedHooks.size) + ex.asInstanceOf[UndefinedHooksException] + assertEquals(6, incorrectHookDefException.undefinedHooks.size) val expectedMsg = """Some hooks are not defined properly: - | - IncorrectClassHooksDefinition.scala:11 (BEFORE) - | - IncorrectClassHooksDefinition.scala:14 (BEFORE_STEP) - | - IncorrectClassHooksDefinition.scala:17 (AFTER) - | - IncorrectClassHooksDefinition.scala:20 (AFTER_STEP) + | - IncorrectClassHooksDefinition.scala:11 (BEFORE_ALL) + | - IncorrectClassHooksDefinition.scala:14 (BEFORE) + | - IncorrectClassHooksDefinition.scala:17 (BEFORE_STEP) + | - IncorrectClassHooksDefinition.scala:20 (AFTER_ALL) + | - IncorrectClassHooksDefinition.scala:23 (AFTER) + | - IncorrectClassHooksDefinition.scala:26 (AFTER_STEP) | |This can be caused by defining hooks where the body returns a Int or String rather than Unit. | @@ -221,9 +225,9 @@ class ScalaBackendTest { |""".stripMargin assertEquals(expectedMsg, incorrectHookDefException.getMessage) case Failure(ex) => - fail(s"Expected IncorrectHookDefinitionException, got ${ex.getClass}") + fail(s"Expected UndefinedHooksException, got ${ex.getClass}") case Success(_) => - fail("Expected IncorrectHookDefinitionException") + fail("Expected UndefinedHooksException") } } @@ -242,15 +246,17 @@ class ScalaBackendTest { } result match { - case Failure(ex) if ex.isInstanceOf[IncorrectHookDefinitionException] => + case Failure(ex) if ex.isInstanceOf[UndefinedHooksException] => val incorrectHookDefException = - ex.asInstanceOf[IncorrectHookDefinitionException] - assertEquals(4, incorrectHookDefException.undefinedHooks.size) + ex.asInstanceOf[UndefinedHooksException] + assertEquals(6, incorrectHookDefException.undefinedHooks.size) val expectedMsg = """Some hooks are not defined properly: - | - IncorrectObjectHooksDefinition.scala:11 (BEFORE) - | - IncorrectObjectHooksDefinition.scala:14 (BEFORE_STEP) - | - IncorrectObjectHooksDefinition.scala:17 (AFTER) - | - IncorrectObjectHooksDefinition.scala:20 (AFTER_STEP) + | - IncorrectObjectHooksDefinition.scala:11 (BEFORE_ALL) + | - IncorrectObjectHooksDefinition.scala:14 (BEFORE) + | - IncorrectObjectHooksDefinition.scala:17 (BEFORE_STEP) + | - IncorrectObjectHooksDefinition.scala:20 (AFTER_ALL) + | - IncorrectObjectHooksDefinition.scala:23 (AFTER) + | - IncorrectObjectHooksDefinition.scala:26 (AFTER_STEP) | |This can be caused by defining hooks where the body returns a Int or String rather than Unit. | @@ -269,9 +275,46 @@ class ScalaBackendTest { |""".stripMargin assertEquals(expectedMsg, incorrectHookDefException.getMessage) case Failure(ex) => - fail(s"Expected IncorrectHookDefinitionException, got ${ex.getClass}") + fail(s"Expected UndefinedHooksException, got ${ex.getClass}") + case Success(_) => + fail("Expected UndefinedHooksException") + } + } + + @Test + def loadGlueAndBuildWorld_class_static_class_hooks_definitions(): Unit = { + val result = Try { + // Load glue + backend.loadGlue( + fakeGlue, + List( + URI.create( + "classpath:io/cucumber/scala/steps/errors/staticclasshooks" + ) + ).asJava + ) + + // Build world + backend.buildWorld() + } + + result match { + case Failure(ex) if ex.isInstanceOf[ScenarioScopedStaticHookException] => + val incorrectHookDefException = + ex.asInstanceOf[ScenarioScopedStaticHookException] + assertEquals(2, incorrectHookDefException.staticHooks.size) + val expectedMsg = """Some hooks are not defined properly: + | - StaticClassHooksDefinition.scala:11 + | - StaticClassHooksDefinition.scala:14 + | + |This can be caused by defining static hooks (BeforeAll/AfterAll) in a class rather than in a object. + |Such hooks can only be defined in a static context. + |""".stripMargin + assertEquals(expectedMsg, incorrectHookDefException.getMessage) + case Failure(ex) => + fail(s"Expected ScenarioScopedStaticHookException, got ${ex.getClass}") case Success(_) => - fail("Expected IncorrectHookDefinitionException") + fail("Expected ScenarioScopedStaticHookException") } } diff --git a/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslHooksTest.scala b/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslHooksTest.scala index 5e82d790..3bd7dd6a 100644 --- a/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslHooksTest.scala +++ b/cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslHooksTest.scala @@ -1,12 +1,11 @@ package io.cucumber.scala -import java.util.concurrent.atomic.AtomicBoolean - import io.cucumber.core.backend._ import org.junit.Assert.{assertEquals, assertTrue} import org.junit.{Before, Test} import org.mockito.Mockito.mock +import java.util.concurrent.atomic.AtomicBoolean import scala.annotation.nowarn @nowarn @@ -861,6 +860,54 @@ class ScalaDslHooksTest { assertObjectHook(Glue.registry.afterStepHooks.head, "tagExpression", 42) } + @Test + def testObjectBeforeAllHook(): Unit = { + + object Glue extends ScalaDsl { + BeforeAll { + invoke() + } + } + + assertObjectStaticHook(Glue.registry.beforeAllHooks.head, 1000) + } + + @Test + def testObjectBeforeAllHookWithOrder(): Unit = { + + object Glue extends ScalaDsl { + BeforeAll(42) { + invoke() + } + } + + assertObjectStaticHook(Glue.registry.beforeAllHooks.head, 42) + } + + @Test + def testObjectAfterAllHook(): Unit = { + + object Glue extends ScalaDsl { + AfterAll { + invoke() + } + } + + assertObjectStaticHook(Glue.registry.afterAllHooks.head, 1000) + } + + @Test + def testObjectAfterAllHookWithOrder(): Unit = { + + object Glue extends ScalaDsl { + AfterAll(42) { + invoke() + } + } + + assertObjectStaticHook(Glue.registry.afterAllHooks.head, 42) + } + private def assertClassHook( hookDetails: ScalaHookDetails, tagExpression: String, @@ -877,6 +924,13 @@ class ScalaDslHooksTest { assertHook(ScalaHookDefinition(hookDetails, false), tagExpression, order) } + private def assertObjectStaticHook( + hookDetails: ScalaStaticHookDetails, + order: Int + ): Unit = { + assertStaticHook(ScalaStaticHookDefinition(hookDetails), order) + } + private def assertHook( hook: HookDefinition, tagExpression: String, @@ -888,4 +942,10 @@ class ScalaDslHooksTest { assertTrue(invoked.get()) } + private def assertStaticHook(hook: StaticHookDefinition, order: Int): Unit = { + assertEquals(order, hook.getOrder) + hook.execute() + assertTrue(invoked.get()) + } + } diff --git a/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectclasshooks/IncorrectClassHooksDefinition.scala b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectclasshooks/IncorrectClassHooksDefinition.scala index e27055cb..2016e0cf 100644 --- a/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectclasshooks/IncorrectClassHooksDefinition.scala +++ b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectclasshooks/IncorrectClassHooksDefinition.scala @@ -7,17 +7,23 @@ class IncorrectClassHooksDefinition extends ScalaDsl { // On a single line to avoid difference between Scala versions for the location + // A body that does not return Unit => interpreted as missing body + BeforeAll { 22 } + // A body that does not return Unit => interpreted as missing body Before { 1 } // A body that does not return Unit => interpreted as missing body BeforeStep { "toto" } + // A body that does not return Unit => interpreted as missing body + AfterAll { 66 } + // A body that does not return Unit => interpreted as missing body After { 33 } // A body that does not return Unit => interpreted as missing body AfterStep { "toto" } - + } //@formatter:on diff --git a/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectobjecthooks/IncorrectObjectHooksDefinition.scala b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectobjecthooks/IncorrectObjectHooksDefinition.scala index 8ee275b6..b0ed85ff 100644 --- a/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectobjecthooks/IncorrectObjectHooksDefinition.scala +++ b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectobjecthooks/IncorrectObjectHooksDefinition.scala @@ -7,12 +7,18 @@ object IncorrectObjectHooksDefinition extends ScalaDsl { // On a single line to avoid difference between Scala versions for the location + // A body that does not return Unit => interpreted as missing body + BeforeAll { 22 } + // A body that does not return Unit => interpreted as missing body Before { 1 } // A body that does not return Unit => interpreted as missing body BeforeStep { "toto" } + // A body that does not return Unit => interpreted as missing body + AfterAll { 66 } + // A body that does not return Unit => interpreted as missing body After { 33 } diff --git a/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/staticclasshooks/StaticClassHooksDefinition.scala b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/staticclasshooks/StaticClassHooksDefinition.scala new file mode 100644 index 00000000..236baaab --- /dev/null +++ b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/staticclasshooks/StaticClassHooksDefinition.scala @@ -0,0 +1,17 @@ +package io.cucumber.scala.steps.errors.staticclasshooks + +import io.cucumber.scala.ScalaDsl + +//@formatter:off +class StaticClassHooksDefinition extends ScalaDsl { + + // On a single line to avoid difference between Scala versions for the location + + // Static hook not allowed in classes + BeforeAll { () } + + // Static hook not allowed in classes + AfterAll { () } + +} +//@formatter:on diff --git a/cucumber-scala/src/test/scala/io/cucumber/scala/steps/objects/StepsInObject.scala b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/objects/StepsInObject.scala index 39878770..590557b0 100644 --- a/cucumber-scala/src/test/scala/io/cucumber/scala/steps/objects/StepsInObject.scala +++ b/cucumber-scala/src/test/scala/io/cucumber/scala/steps/objects/StepsInObject.scala @@ -4,6 +4,11 @@ import io.cucumber.scala.{EN, ScalaDsl} object StepsInObject extends ScalaDsl with EN { + BeforeAll { + // Nothing + () + } + Before { // Nothing () @@ -14,6 +19,11 @@ object StepsInObject extends ScalaDsl with EN { () } + AfterAll { + // Nothing + () + } + After { // Nothing () diff --git a/cucumber-scala/src/test/scala/tests/statichooks/RunStaticHooksTest.scala b/cucumber-scala/src/test/scala/tests/statichooks/RunStaticHooksTest.scala new file mode 100644 index 00000000..b2a1badc --- /dev/null +++ b/cucumber-scala/src/test/scala/tests/statichooks/RunStaticHooksTest.scala @@ -0,0 +1,32 @@ +package tests.statichooks + +import io.cucumber.junit.{Cucumber, CucumberOptions} +import org.junit.{AfterClass, BeforeClass} +import org.junit.Assert.assertEquals +import org.junit.runner.RunWith + +@RunWith(classOf[Cucumber]) +@CucumberOptions() +class RunStaticHooksTest + +object RunStaticHooksTest { + + @BeforeClass + def beforeAllJunit(): Unit = { + assertEquals( + "Before Cucumber's BeforeAll", + 0L, + StaticHooksSteps.countBeforeAll.toLong + ) + } + + @AfterClass + def afterAllJunit(): Unit = { + assertEquals( + "After Cucumber's AfterAll", + 1L, + StaticHooksSteps.countAfterAll.toLong + ) + } + +} diff --git a/cucumber-scala/src/test/scala/tests/statichooks/StaticHooksSteps.scala b/cucumber-scala/src/test/scala/tests/statichooks/StaticHooksSteps.scala new file mode 100644 index 00000000..db379b4c --- /dev/null +++ b/cucumber-scala/src/test/scala/tests/statichooks/StaticHooksSteps.scala @@ -0,0 +1,37 @@ +package tests.statichooks + +import io.cucumber.scala.{EN, ScalaDsl} +import org.junit.Assert.assertEquals + +import scala.annotation.nowarn + +@nowarn +object StaticHooksSteps extends ScalaDsl with EN { + + var countBeforeAll: Int = 0 + var countAfterAll: Int = 0 + + BeforeAll { + countBeforeAll = countBeforeAll + 1 + } + + AfterAll { + countAfterAll = countAfterAll + 1 + } + + When("""I run scenario {string}""") { (scenarioName: String) => + println(s"Running scenario $scenarioName") + () + } + + Then("""BeforeAll count is {int}""") { (count: Int) => + println(s"BeforeAll = $countBeforeAll") + assertEquals(count, countBeforeAll) + } + + Then("""AfterAll count is {int}""") { (count: Int) => + println(s"AfterAll = $countAfterAll") + assertEquals(count, countAfterAll) + } + +} diff --git a/docs/hooks.md b/docs/hooks.md index 8b68f4a4..80a24e83 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -1,10 +1,36 @@ # Hooks Hooks are blocks of code that can run at various points in the Cucumber execution cycle. -They are typically used for setup and teardown of the environment before and after each scenario. +They are typically used for setup and teardown of the environment before and after all/each scenario or step. See the [reference documentation](https://docs.cucumber.io/docs/cucumber/api/#hooks). +## Static hooks + +Static hooks run once before/after all scenarios. + +### BeforeAll + +`BeforeAll` hooks run once before all scenarios. + +```scala +BeforeAll { + // Do something before all scenarios + // Must return Unit +} +``` + +### AfterAll + +`AfterAll` hooks run once after all scenarios. + +```scala +AfterAll { + // Do something after each scenario + // Must return Unit +} +``` + ## Scenario hooks Scenario hooks run for every scenario. @@ -88,6 +114,8 @@ Before("@browser and not @headless") { } ``` +Note: this cannot be applied to static hooks (`BeforeAll`/`AfterAll`). + ## Order You can define an order between multiple hooks. @@ -115,3 +143,5 @@ Before("@browser and not @headless", 10) { // Must return Unit } ``` + +Note: this cannot be applied to static hooks (`BeforeAll`/`AfterAll`).