Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ val jacksonVersion = "2.20.0"
val jackson3Version = "3.0.0"
val mockitoScalaVersion = "2.0.0"
val junit4Version = "4.13.2"
val scalatestVersion = "3.2.19"

// BOMs

Expand Down Expand Up @@ -75,6 +76,9 @@ lazy val junit4SbtSupport = Seq(
lazy val junit5SbtSupport = Seq(
libraryDependencies += "com.github.sbt.junit" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % Test
)
lazy val scalatestSbtSupport = Seq(
libraryDependencies += "org.scalatest" %% "scalatest" % scalatestVersion % Test
)

lazy val root = (project in file("."))
.settings(commonSettings)
Expand All @@ -83,12 +87,14 @@ lazy val root = (project in file("."))
)
.aggregate(
cucumberScala.projectRefs ++
cucumberScalatest.projectRefs ++
integrationTestsCommon.projectRefs ++
integrationTestsJackson2.projectRefs ++
integrationTestsJackson3.projectRefs ++
integrationTestsPicoContainer.projectRefs ++
examplesJunit4.projectRefs ++
examplesJunit5.projectRefs: _*
examplesJunit5.projectRefs ++
examplesScalatest.projectRefs: _*
)

// Main project
Expand Down Expand Up @@ -145,6 +151,21 @@ lazy val cucumberScala = (projectMatrix in file("cucumber-scala"))
)
.jvmPlatform(scalaVersions = Seq(scala3, scala213, scala212))

// Scalatest integration
lazy val cucumberScalatest = (projectMatrix in file("cucumber-scalatest"))
.settings(commonSettings)
.settings(scalatestSbtSupport)
.settings(
name := "cucumber-scalatest",
libraryDependencies ++= Seq(
"io.cucumber" % "cucumber-core" % cucumberVersion,
"org.scalatest" %% "scalatest-core" % scalatestVersion
),
publishArtifact := true
)
.dependsOn(cucumberScala)
.jvmPlatform(scalaVersions = Seq(scala3, scala213, scala212))

// Integration tests
lazy val integrationTestsCommon =
(projectMatrix in file("integration-tests/common"))
Expand Down Expand Up @@ -238,6 +259,20 @@ lazy val examplesJunit5 = (projectMatrix in file("examples/examples-junit5"))
.dependsOn(cucumberScala % Test)
.jvmPlatform(scalaVersions = Seq(scala3, scala213))

lazy val examplesScalatest = (projectMatrix in file("examples/examples-scalatest"))
.settings(commonSettings)
.settings(scalatestSbtSupport)
.settings(
name := "scala-examples-scalatest",
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % scalatestVersion % Test
),
publishArtifact := false
)
.dependsOn(cucumberScala % Test)
.dependsOn(cucumberScalatest % Test)
.jvmPlatform(scalaVersions = Seq(scala3, scala213))

// Version policy check

ThisBuild / versionScheme := Some("early-semver")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package io.cucumber.scalatest

import io.cucumber.core.options.RuntimeOptionsBuilder
import io.cucumber.core.runtime.{Runtime => CucumberRuntime}
import org.scalatest.{Args, Status, Suite}

import scala.annotation.nowarn

/** Configuration for Cucumber tests.
*
* @param features
* paths to feature files or directories (e.g., "classpath:features")
* @param glue
* packages containing step definitions (e.g., "com.example.steps")
* @param plugin
* plugins to use (e.g., "pretty", "json:target/cucumber.json")
*/
case class CucumberOptions(
features: List[String] = List.empty,
glue: List[String] = List.empty,
plugin: List[String] = List.empty
)

/** A trait that allows Cucumber scenarios to be run with ScalaTest.
*
* Mix this trait into your test class and define the `cucumberOptions` value
* to configure the Cucumber runtime.
*
* Example:
* {{{
* import io.cucumber.scalatest.{CucumberOptions, CucumberSuite}
*
* class RunCucumberTest extends CucumberSuite {
* override val cucumberOptions = CucumberOptions(
* features = List("classpath:features"),
* glue = List("com.example.stepdefinitions"),
* plugin = List("pretty")
* )
* }
* }}}
*/
@nowarn
trait CucumberSuite extends Suite {

/** Override this value to configure Cucumber options. If not overridden,
* defaults will be used based on the package name.
*/
def cucumberOptions: CucumberOptions = CucumberOptions()

/** Runs the Cucumber scenarios.
*
* @param testName
* An optional name of one test to run. If None, all relevant tests should
* be run.
* @param args
* the Args for this run
* @return
* a Status object that indicates when all tests started by this method
* have completed, and whether or not a failure occurred.
*/
abstract override def run(
testName: Option[String],
args: Args
): Status = {
if (testName.isDefined) {
throw new IllegalArgumentException(
"Suite traits implemented by Cucumber do not support running a single test"
)
}

val runtimeOptions = buildRuntimeOptions()
val classLoader = getClass.getClassLoader

val runtime = CucumberRuntime
.builder()
.withRuntimeOptions(runtimeOptions)
.withClassLoader(new java.util.function.Supplier[ClassLoader] {
override def get(): ClassLoader = classLoader
})
.build()

runtime.run()

val exitStatus = runtime.exitStatus()
if (exitStatus == 0) {
org.scalatest.SucceededStatus
} else {
throw new RuntimeException(
s"Cucumber scenarios failed with exit status: $exitStatus"
)
}
}

private def buildRuntimeOptions(): io.cucumber.core.options.RuntimeOptions = {
val packageName = getClass.getPackage.getName
val builder = new RuntimeOptionsBuilder()

// Add features
val features =
if (cucumberOptions.features.nonEmpty) cucumberOptions.features
else List("classpath:" + packageName.replace('.', '/'))

features.foreach { feature =>
builder.addFeature(
io.cucumber.core.feature.FeatureWithLines.parse(feature)
)
}

// Add glue
val glue =
if (cucumberOptions.glue.nonEmpty) cucumberOptions.glue
else List(packageName)

glue.foreach { g =>
builder.addGlue(java.net.URI.create("classpath:" + g))
}

// Add plugins
cucumberOptions.plugin.foreach { p =>
builder.addPluginName(p)
}

builder.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cucumber.examples.scalacalculator

import scala.collection.mutable.Queue

sealed trait Arg

object Arg {
implicit def op(s: String): Op = Op(s)
implicit def value(v: Double): Val = Val(v)
}

case class Op(value: String) extends Arg
case class Val(value: Double) extends Arg

class RpnCalculator {
private val stack = Queue.empty[Double]

private def op(f: (Double, Double) => Double) =
stack += f(stack.dequeue(), stack.dequeue())

def push(arg: Arg): Unit = {
arg match {
case Op("+") => op(_ + _)
case Op("-") => op(_ - _)
case Op("*") => op(_ * _)
case Op("/") => op(_ / _)
case Val(value) => stack += value
case _ => ()
}
()
}

def value: Double = stack.head
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@foo
Feature: Basic Arithmetic

Scenario: Adding
# Try to change one of the values below to provoke a failure
When I add 4.0 and 5.0
Then the result is 9.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
cucumber.plugin=pretty
# Workaround for https://github.com/sbt/sbt-jupiter-interface/issues/142
# See also https://github.com/cucumber/cucumber-jvm/pull/3023
cucumber.junit-platform.discovery.as-root-engine=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cucumber.examples.scalacalculator

import io.cucumber.scala.{EN, ScalaDsl, Scenario}

class RpnCalculatorStepDefinitions extends ScalaDsl with EN {

val calc = new RpnCalculator

When("""I add {double} and {double}""") { (arg1: Double, arg2: Double) =>
calc push arg1
calc push arg2
calc push "+"
}

Then("the result is {double}") { (expected: Double) =>
assert(
math.abs(expected - calc.value) < 0.001,
s"Expected $expected but got ${calc.value}"
)
}

Before("not @foo") { (scenario: Scenario) =>
println(s"Runs before scenarios *not* tagged with @foo (${scenario.getId})")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package cucumber.examples.scalacalculator

import io.cucumber.scalatest.{CucumberOptions, CucumberSuite}

class RunCukesTest extends CucumberSuite {
override val cucumberOptions = CucumberOptions(
features = List("classpath:cucumber/examples/scalacalculator"),
glue = List("cucumber.examples.scalacalculator"),
plugin = List("pretty")
)
}