Skip to content

Commit

Permalink
Merge pull request #77 from agourlay/0.8.0
Browse files Browse the repository at this point in the history
0.8.0
  • Loading branch information
agourlay authored Jun 13, 2016
2 parents 02dc7b3 + 7e76c63 commit 1110668
Show file tree
Hide file tree
Showing 60 changed files with 1,999 additions and 724 deletions.
206 changes: 133 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ An extensible Scala DSL for testing JSON HTTP APIs.
5. [Wrapper steps](#wrapper-steps)
6. [Debug steps](#debug-steps)
5. [DSL composition](#dsl-composition)
6. [Custom steps](#custom-steps)
7. [Placeholders](#placeholders)
6. [Placeholders](#placeholders)
7. [Custom steps](#custom-steps)
1. [Effects and Assertions](#effects-and-assertions)
2. [HTTP service](#http-service)
8. [Feature options](#feature-options)
1. [Before and after hooks](#before-and-after-hooks)
2. [Base URL](#base-url)
Expand All @@ -35,10 +37,10 @@ An extensible Scala DSL for testing JSON HTTP APIs.
Add the library dependency

``` scala
libraryDependencies += "com.github.agourlay" %% "cornichon" % "0.7.4" % "test"
libraryDependencies += "com.github.agourlay" %% "cornichon" % "0.8.0" % "test"
```

Cornichon is currently integrated with [ScalaTest](http://www.scalatest.org/), just place your ```Feature``` inside ```src/test/scala``` and run them using ```sbt test```.
Cornichon is currently integrated with [ScalaTest](http://www.scalatest.org/), place your ```Feature``` files inside ```src/test/scala``` and run them using ```sbt test```.

A ```Feature``` is a class extending ```CornichonFeature``` and implementing the required ```feature``` function.

Expand Down Expand Up @@ -115,10 +117,15 @@ class ReadmeExample extends CornichonFeature {

For more examples see the following files which are part of the test pipeline:

- [Embedded Superheroes API](https://github.com/agourlay/cornichon/blob/master/src/test/scala/com/github/agourlay/cornichon/examples/superHeroes/SuperHeroesScenario.scala).

- [OpenMovieDatabase API](https://github.com/agourlay/cornichon/blob/master/src/it/scala/com.github.agourlay.cornichon.examples/OpenMovieDatabase.scala).

- [Embedded Superheroes API](https://github.com/agourlay/cornichon/blob/master/src/test/scala/com/github/agourlay/cornichon/examples/superHeroes/SuperHeroesScenario.scala).
- [DeckOfCard API](https://github.com/agourlay/cornichon/blob/master/src/it/scala/com.github.agourlay.cornichon.examples/DeckOfCard.scala).

- [Star Wars API](https://github.com/agourlay/cornichon/blob/master/src/it/scala/com.github.agourlay.cornichon.examples/StarWars.scala).

- [Math Operations](https://github.com/agourlay/cornichon/blob/master/src/test/scala/com/github/agourlay/cornichon/examples/math/MathScenario.scala).

## Structure

Expand Down Expand Up @@ -496,6 +503,24 @@ Within(maxDuration = 10 seconds) {

```

- repeat a series of steps with different inputs specified via a datatable

```scala
WithDataInputs(
"""
| a | b | c |
| 1 | 3 | 4 |
| 7 | 4 | 11 |
| 1 | -1 | 0 |
"""
) {
Then assert a_plus_b_equals_c
}

def a_plus_b_equals_c =
AssertStep("sum of 'a' + 'b' = 'c'", s SimpleStepAssertion(s.get("a").toInt + s.get("b").toInt, s.get("c").toInt))
```

- WithHeaders automatically sets headers for several steps useful for authenticated scenario.

```scala
Expand Down Expand Up @@ -585,74 +610,6 @@ class CornichonExamplesSpec extends CornichonFeature {

```

## Custom steps

There are two kind of ```step``` :
- EffectStep ```Session => Session``` : It runs a side effect and populates the ```Session``` with values.
- AssertStep ```Sesssion => StepAssertion[A]``` : Describes the expectation of the test.


A ```session``` is a Map-like object used to propagate state throughout a ```scenario```. It is used to resolve [placeholders](#placeholders)

A ```StepAssertion``` is simply a container for 2 values, the expected value and the actual result. The test engine is responsible to test the equality of the ```StepAssertion``` values.

The engine will try its best to provide a meaningful error message, if a specific error message is required tt is also possible to provide a custom error message using a ```DetailedStepAssertion```.

```scala
DetailedStepAssertion[A](expected: A, result: A, details: A String)
```

The engine will feed the actual result to the ```details``` function.

In practice the simplest runnable statement in the DSL is

```scala
When I AssertStep("do nothing", s => StepAssertion(true, true))
```

Let's try to assert the result of a computation

```scala
When I AssertStep("calculate", s => StepAssertion(2 + 2, 4))
```

The ```session``` is used to store the result of a computation in order to reuse it or to apply more advanced assertions on it later.


```scala
When I EffectStep(
title = "run crazy computation",
action = s => {
val pi = piComputation()
s.add("result", res)
})

Then assert AssertStep(
title = "check computation infos",
action = s => {
val pi = s.get("result")
StepAssertion(pi, 3.14)
})
```

This is rather low level and you not should write your steps like that directly inside the DSL.

Fortunately a bunch of built-in steps and primitive building blocs are already available.

Most of the time you will create your own trait containing your custom steps :

```scala
trait MySteps {
this: CornichonFeature

// here access all the goodies from the DSLs and the HttpService.
}

```

Note for advance users: it is also possible to write custom wrapper steps.


## Placeholders

Most built-in steps can use placeholders in their arguments, those will be automatically resolved from the ```session```:
Expand Down Expand Up @@ -722,6 +679,109 @@ It becomes then possible to retrieve past values :
- ```<name[0]>``` uses the first value taken by the key
- ```<name[1]>``` uses the second element taken by the key

## Custom steps

### Effects and Assertions

There are two kind of ```step``` :
- EffectStep ```Session => Session``` : It runs a side effect and populates the ```Session``` with values.
- AssertStep ```Sesssion => StepAssertion[A]``` : Describes the expectation of the test.


A ```session``` is a Map-like object used to propagate state throughout a ```scenario```. It is used to resolve [placeholders](#placeholders)

A ```StepAssertion``` is simply a container for 2 values, the expected value and the actual result. The test engine is responsible to test the equality of the ```StepAssertion``` values.

The engine will try its best to provide a meaningful error message, if a specific error message is required it is also possible to provide a custom error message using a ```DetailedStepAssertion```.

```scala
DetailedStepAssertion[A](expected: A, result: A, details: A String)
```

The engine will feed the actual result to the ```details``` function.

In practice the simplest runnable statement in the DSL is

```scala
When I AssertStep("do nothing", s => StepAssertion(true, true))
```

Let's try to assert the result of a computation

```scala
When I AssertStep("calculate", s => StepAssertion(2 + 2, 4))
```

The ```session``` is used to store the result of a computation in order to reuse it or to apply more advanced assertions on it later.


```scala
When I EffectStep(
title = "run crazy computation",
action = s =>
val pi = piComputation()
s.add("result", res)
)

Then assert AssertStep(
title = "check computation infos",
action = s =>
val pi = s.get("result")
StepAssertion(pi, 3.14)
)
```

This is rather low level and you not should write your steps like that directly inside the DSL.

Fortunately a bunch of built-in steps and primitive building blocs are already available for you.

Note for advance users: it is also possible to write custom wrapper steps by implementing ```WrapperStep```.


### HTTP service

Sometimes you still want to perform HTTP calls inside of custom effect steps, this is where the ```http``` service comes in handy.

In order to illustrate its usage let's take the following example, you would like to write a custom step like:

```scala
def feature = Feature("Customer endpoint"){

Scenario("create customer"){

When I create_customer

Then assert status.is(201)

}
```

Most of the time you will create your own trait containing your custom steps and declare a self-type on ```CornichonFeature``` to be able to access the ```http``` service.

```scala
trait MySteps {
this: CornichonFeature

def create_customer = EffectStep(
title = "create new customer",
effect = s
http.Post(
url = "/customer",
payload = some_json_payload_to_define,
params = Seq.empty,
headers = Seq.empty,
extractor = RootExtractor("customer")
)(s)
)
}

```

The built-in HTTP steps available on the DSL are actually built on top of the ```http``` service which means that you benefit from all the existing placeholder resolution features.

The ```http``` service call returns effect steps that you can reuse and can be parametrized with different response extractors to fill the session automatically.


## Feature options

To implement a ```CornichonFeature``` it is only required to implement the ```feature``` function. However a number of useful options are available using override.
Expand Down
54 changes: 28 additions & 26 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,43 @@ ScalariformKeys.preferences := ScalariformKeys.preferences.value

libraryDependencies ++= {
val scalaTestV = "2.2.6"
val akkaHttpV = "2.4.6"
val akkaHttpV = "2.4.7"
val catsV = "0.6.0"
val sprayJsonV = "1.3.2"
val json4sV = "3.3.0"
val logbackV = "1.1.7"
val parboiledV = "2.1.3"
val akkaSseV = "1.8.0"
val scalacheckV = "1.12.5"
val sangriaV = "0.6.3"
val sangriaJsonV = "0.3.1"
val sangriaSprayJsonV = "0.3.1"
val fansiV = "0.1.1"
val akkaSseV = "1.8.1"
val scalaCheckV = "1.12.5"
val sangriaCirceV = "0.4.4"
val circeVersion = "0.5.0-M1"
val sangriaV = "0.7.0"
val fansiV = "0.1.3"
val akkaHttpCirce = "1.7.0"
val catsScalaTest = "1.3.0"
Seq(
"com.typesafe.akka" %% "akka-http-core" % akkaHttpV
,"de.heikoseeberger" %% "akka-sse" % akkaSseV
,"org.json4s" %% "json4s-jackson" % json4sV
,"io.spray" %% "spray-json" % sprayJsonV
,"org.typelevel" %% "cats-macros" % catsV
,"org.typelevel" %% "cats-core" % catsV
,"org.scalatest" %% "scalatest" % scalaTestV
,"ch.qos.logback" % "logback-classic" % logbackV
,"org.parboiled" %% "parboiled" % parboiledV
,"org.scalacheck" %% "scalacheck" % scalacheckV
,"com.lihaoyi" %% "fansi" % fansiV
,"org.sangria-graphql" %% "sangria" % sangriaV
,"org.sangria-graphql" %% "sangria-json4s-jackson" % sangriaJsonV
,"org.sangria-graphql" %% "sangria-spray-json" % sangriaSprayJsonV % "test"
,"com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaHttpV % "test"
,"com.typesafe.akka" %% "akka-http-experimental" % akkaHttpV % "test"
"com.typesafe.akka" %% "akka-http-core" % akkaHttpV
,"de.heikoseeberger" %% "akka-sse" % akkaSseV
,"org.typelevel" %% "cats-macros" % catsV
,"org.typelevel" %% "cats-core" % catsV
,"org.scalatest" %% "scalatest" % scalaTestV
,"ch.qos.logback" % "logback-classic" % logbackV
,"org.parboiled" %% "parboiled" % parboiledV
,"org.scalacheck" %% "scalacheck" % scalaCheckV
,"com.lihaoyi" %% "fansi" % fansiV
,"org.sangria-graphql" %% "sangria" % sangriaV
,"org.sangria-graphql" %% "sangria-circe" % sangriaCirceV
,"io.circe" %% "circe-core" % circeVersion
,"io.circe" %% "circe-generic" % circeVersion
,"io.circe" %% "circe-parser" % circeVersion
//,"io.circe" %% "circe-optics" % circeVersion Remove if cursors are used instead or lenses for JsonPath.
,"de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce % "test"
,"com.typesafe.akka" %% "akka-http-experimental" % akkaHttpV % "test"
,"com.ironcorelabs" %% "cats-scalatest" % catsScalaTest % "test"
)
}

// Wartremover
wartremoverErrors in (Compile, compile) ++= Seq(
Wart.Any2StringAdd, Wart.Option2Iterable, Wart.OptionPartial,
Wart.Any2StringAdd, Wart.Option2Iterable,
Wart.Return, Wart.TryPartial)

// Publishing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.github.agourlay.cornichon.examples

import com.github.agourlay.cornichon.CornichonFeature
import scala.concurrent.duration._

//see http://deckofcardsapi.com/
class DeckOfCard extends CornichonFeature {

override lazy val baseUrl = "http://deckofcardsapi.com/api"

def feature =
Feature("Deck of Card API") {

Scenario("draw any king") {

Given I get("/deck/new/shuffle/?deck_count=1").withParams(
"deck_count" -> "1"
)

Then assert status.is(200)

And I save_body_path("deck_id" -> "deck-id")

Eventually(maxDuration = 10.seconds, interval = 10.millis) {

When I get("/deck/<deck-id>/draw/")

And assert status.is(200)

Then assert body.path("cards[0].value").is("KING")

}
}

// Idea: implement blackjack game :)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ class OpenMovieDatabase extends CornichonFeature {
And assert body.ignoring("Episodes", "Response").is("""
{
"Title": "Game of Thrones",
"Season": "1"
"Season": "1",
"totalSeasons" : "7"
}
""")

Expand All @@ -74,6 +75,7 @@ class OpenMovieDatabase extends CornichonFeature {
{
"Title": "Game of Thrones",
"Season": "1",
"totalSeasons" : "7",
"Episodes": [
{
"Title": "Winter Is Coming",
Expand Down
Loading

0 comments on commit 1110668

Please sign in to comment.