Skip to content

Commit

Permalink
Merge pull request #78 from agourlay/infix-dynamic-type
Browse files Browse the repository at this point in the history
Infix dynamic type
  • Loading branch information
agourlay authored Jun 18, 2016
2 parents 35082cc + f422b66 commit 51e007b
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 53 deletions.
43 changes: 23 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,30 +159,31 @@ A ```scenario``` will stop at the first failed step encountered and ignore the r

## DSL

Statements start with one of the prefixes below followed by a ```step``` definition :
The structure of a statement is the following:

* Given
- I ```step```
- a ```step```

* When
- I ```step```
- a ```step```

* And
- I ```step```
- a ```step```
- assert ```step```
- assert_not ```step``` (expects the step to fail)

* Then
- I ```step```
- a ```step```
- assert ```step```
- assert_not ```step``` (expects the step to fail)
1 - starts with either ```Given``` - ```When``` - ```And``` - ```Then```

2 - followed by any single word (could be several words wrapped in backticks)

3 - ending with a ```step``` definition

For example :

```scala
Given I step_definition

When a step_definition

And \`another really important\` step_definition

Then assert step_definition

```

Those prefixes do not change the behaviour of the steps and are here to improve readability.

This structure was chosen to increase the freedom of customisation while still benefiting from Scala's infix notation.

The usage pattern is often to first run a ```step``` with a side effect then assert an expected state in a second ```step```.

## Built-in steps
Expand Down Expand Up @@ -610,6 +611,8 @@ class CornichonExamplesSpec extends CornichonFeature {

```

It is possible to give a title to an attached bloc using ```AttachAs(title)```.

## Placeholders

Most built-in steps can use placeholders in their arguments, those will be automatically resolved from the ```session```:
Expand Down
48 changes: 17 additions & 31 deletions src/main/scala/com/github/agourlay/cornichon/dsl/Dsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.github.agourlay.cornichon.steps.wrapped._
import com.github.agourlay.cornichon.util.Formats._

import scala.language.experimental.{ macros `scalac, please just let me do it!` }
import scala.language.dynamics
import scala.concurrent.duration.{ Duration, FiniteDuration }

trait Dsl {
Expand All @@ -18,45 +19,30 @@ trait Dsl {
def Scenario(name: String, ignored: Boolean = false) =
BodyElementCollector[Step, Scenario](steps ScenarioDef(name, steps, ignored))

sealed trait Starters {
sealed trait Starters extends Dynamic {
def name: String

def I(steps: Seq[Step]) = steps

def I(step: Step) = step

def I[A](step: EffectStep) =
step.copy(s"$name I ${step.title}")

def a[A](step: EffectStep) =
step.copy(s"$name a ${step.title}")

def I(ds: DebugStep) = ds
def applyDynamic(mandatoryWord: String)(step: Step) = step match {
case e: EffectStep e.copy(s"$name $mandatoryWord ${e.title}")
case a: AssertStep[_] a.copy(s"$name $mandatoryWord ${a.title}")
case a: AttachAsStep a.copy(s"$name $mandatoryWord ${a.title}")
case _ step
}
}

case object When extends Starters { val name = "When" }
case object Given extends Starters { val name = "Given" }
case object Then extends Starters { val name = "Then" }
case object And extends Starters { val name = "And" }

sealed trait WithAssert {
self: Starters

def assert(steps: Seq[Step]) = steps

def assert(step: Step) = step

def assert[A](step: AssertStep[A]) =
step.copy(s"$name assert ${step.title}")

def assert_not[A](step: AssertStep[A]) =
step.copy(s"$name assert not ${step.title}").copy(negate = true)
}

case object Then extends Starters with WithAssert { val name = "Then" }
case object And extends Starters with WithAssert { val name = "And" }
def Attach: BodyElementCollector[Step, Step] =
BodyElementCollector[Step, Step] { steps
AttachStep(nested = steps)
}

def Attach =
BodyElementCollector[Step, Seq[Step]] { steps
steps
def AttachAs(title: String) =
BodyElementCollector[Step, Step] { steps
AttachAsStep(title, steps)
}

def Repeat(times: Int) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,19 @@ object HttpAssertions {
)
}

def not_contains(elements: A*) = {
val prettyElements = elements.mkString(" and ")
val title = if (jsonPath == JsonPath.root) s"response body array does not contain $prettyElements" else s"response body's array '$jsonPath' does not contain $prettyElements"
containsInSession(title, elements).copy(negate = true)
}

def contains(elements: A*) = {
val prettyElements = elements.mkString(" and ")
val title = if (jsonPath == JsonPath.root) s"response body array contains $prettyElements" else s"response body's array '$jsonPath' contains $prettyElements"
containsInSession(title, elements)
}

private def containsInSession(title: String, elements: Seq[A]): AssertStep[Boolean] = {
from_session_detail_step(
title = title,
key = LastResponseBodyKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.agourlay.cornichon.steps.wrapped

import com.github.agourlay.cornichon.core.{ FailureStepsResult, _ }

import scala.concurrent.ExecutionContext

// Steps are wrapped/idented with a specific title
case class AttachAsStep(title: String, nested: Vector[Step]) extends WrapperStep {

def run(engine: Engine, session: Session, depth: Int)(implicit ec: ExecutionContext) = {
val (res, executionTime) = engine.withDuration {
engine.runSteps(nested, session, Vector.empty, depth + 1)
}
res match {
case s: SuccessStepsResult
val updatedLogs = successTitleLog(depth) +: s.logs :+ SuccessLogInstruction(s"'$title' succeeded", depth, Some(executionTime))
s.copy(logs = updatedLogs)
case f: FailureStepsResult
f.copy(logs = failedTitleLog(depth) +: f.logs :+ FailureLogInstruction(s"'$title' failed", depth))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.agourlay.cornichon.steps.wrapped

import com.github.agourlay.cornichon.core.{ Engine, Session, Step, WrapperStep }

import scala.concurrent.ExecutionContext

// Transparent Attach has no title - steps are flatten in the main execution
case class AttachStep(title: String = "", nested: Vector[Step]) extends WrapperStep {

def run(engine: Engine, session: Session, depth: Int)(implicit ec: ExecutionContext) =
engine.runSteps(nested, session, Vector.empty, depth)

}
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ class SuperHeroesScenario extends CornichonFeature {

Then assert body.asArray.hasSize(4)

And assert_not body.asArray.contains(
And assert body.asArray.not_contains(
"""
{
"name": "IronMan",
Expand Down Expand Up @@ -657,7 +657,7 @@ class SuperHeroesScenario extends CornichonFeature {
}

def superhero_exists(name: String) =
Attach {
AttachAs("superhero exits") {
When I get("/superheroes/Batman").withParams("sessionId" "<session-id>")
Then I show_session
Then assert status.is(200)
Expand Down

0 comments on commit 51e007b

Please sign in to comment.