Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cornichon - A scala DSL for testing HTTP JSON API

Cornichon - A scala DSL for testing HTTP JSON API

Arnaud Gourlay

January 11, 2017
Tweet

Other Decks in Programming

Transcript

  1. Cornichon A scala DSL for testing HTTP JSON API Berlin

    Scala User Group
 11.01.2017 @ArnaudGourlay
  2. Brief history • Building an e-commerce API @commercetools • Important

    Ruby Cucumber codebase • Lack of Ruby expertise 2
  3. Brief history • Building an e-commerce API @commercetools • Important

    Ruby Cucumber codebase • Lack of Ruby expertise • Difficulty to scale a DSL based on String parsing 2
  4. Brief history • Building an e-commerce API @commercetools • Important

    Ruby Cucumber codebase • Lack of Ruby expertise • Difficulty to scale a DSL based on String parsing • Lack of enthusiasm for writing acceptance tests 2
  5. Brief history • Building an e-commerce API @commercetools • Important

    Ruby Cucumber codebase • Lack of Ruby expertise • Difficulty to scale a DSL based on String parsing • Lack of enthusiasm for writing acceptance tests • Frustration breeds creativity… 2
  6. Need for a different tool? • In Scala! • Code

    over plain text • Powerful built-in DSL 3
  7. Need for a different tool? • In Scala! • Code

    over plain text • Powerful built-in DSL • Extensible DSL 3
  8. Need for a different tool? • In Scala! • Code

    over plain text • Powerful built-in DSL • Extensible DSL • Great error reporting 3
  9. Need for a different tool? • In Scala! • Code

    over plain text • Powerful built-in DSL • Extensible DSL • Great error reporting • Maintainable acceptance tests 3
  10. Need for a different tool? • In Scala! • Code

    over plain text • Powerful built-in DSL • Extensible DSL • Great error reporting • Maintainable acceptance tests • Smooth integration in a build pipeline 3
  11. Live demo • Won’t explain much during demo… • But

    the next 50 slides afterwards will! 5
  12. Live demo • Won’t explain much during demo… • But

    the next 50 slides afterwards will! • No boring domain as example please… 5
  13. Live demo • Won’t explain much during demo… • But

    the next 50 slides afterwards will! • No boring domain as example please… • Let’s play cards! 5
  14. Live demo • Won’t explain much during demo… • But

    the next 50 slides afterwards will! • No boring domain as example please… • Let’s play cards! • My first feature using “http://deckofcardsapi.com” 5
  15. Live demo • Won’t explain much during demo… • But

    the next 50 slides afterwards will! • No boring domain as example please… • Let’s play cards! • My first feature using “http://deckofcardsapi.com” • Source for later reference partly at https://github.com/agourlay/ cornichon/blob/master/src/it/scala/ com.github.agourlay.cornichon.examples/DeckOfCard.scala 5
  16. What have we learned? • debugging instruction • asserting response

    parts • dealing with unstable fields • reusing and abstracting over steps 6
  17. What have we learned? • debugging instruction • asserting response

    parts • dealing with unstable fields • reusing and abstracting over steps • SBT is your friend 6
  18. What have we learned? • debugging instruction • asserting response

    parts • dealing with unstable fields • reusing and abstracting over steps • SBT is your friend • baseUrl 6
  19. What have we learned? • debugging instruction • asserting response

    parts • dealing with unstable fields • reusing and abstracting over steps • SBT is your friend • baseUrl • using the session 6
  20. Terminology (1) • feature container for scenarios • scenario container

    for steps • step abstract executable instruction 7
  21. Terminology (1) • feature container for scenarios • scenario container

    for steps • step abstract executable instruction • effect step session => Future[Session] 7
  22. Terminology (1) • feature container for scenarios • scenario container

    for steps • step abstract executable instruction • effect step session => Future[Session] case class EffectStep(title: String, effect: Session ⇒ Future[Session]) extends Step 7
  23. Terminology (2) • assert step session => Assertion • assertion

    runs a predicate case class AssertStep(title: String, action: Session ⇒ Assertion) extends Step trait Assertion {
 def validated: ValidatedNel[CornichonError, Done]
 } 8
  24. Terminology (3) • session state in Map[String, Vector[String]] • placeholders

    key referencing value in the session When I get("/deck/<deck-id>/draw/") 9
  25. Execution model • features are executed sequentially • scenarios within

    are executed in parallel • can be changed via configuration 10
  26. Execution model • features are executed sequentially • scenarios within

    are executed in parallel • can be changed via configuration • depends also on the tested system 10
  27. HTTP effect DSL • GET, DELETE, HEAD, OPTIONS, POST, PUT

    and PATCH • Use a builder shape with body, params, headers for consistency 12
  28. HTTP effect DSL • GET, DELETE, HEAD, OPTIONS, POST, PUT

    and PATCH • Use a builder shape with body, params, headers for consistency • Params in URL are supported for convenience 12
  29. HTTP effect DSL • GET, DELETE, HEAD, OPTIONS, POST, PUT

    and PATCH • Use a builder shape with body, params, headers for consistency • Params in URL are supported for convenience • Body supported on every HTTP verb 12
  30. HTTP effect DSL post("/superheroes").withBody(
 """
 {
 "name": "Scalaman" } """

    ) get("/series").withParams(
 "titles" -> "Game of Thrones",
 "season" -> "1"
 ) 13
  31. HTTP withBody type • Built-in support for String • Body

    type can be parametrised via typeclasses
 
 case class HttpRequest[A: Show: Resolvable: Encoder](body: Option[A],…) 14
  32. HTTP withBody type • Built-in support for String • Body

    type can be parametrised via typeclasses
 
 case class HttpRequest[A: Show: Resolvable: Encoder](body: Option[A],…) • Cats.Show / Circe.Encoder / Cornichon.Resolvable 14
  33. JSON assert DSL (1) • Body is assumed to be

    JSON (see limitations) • JSON Path 16
  34. JSON assert DSL (1) • Body is assumed to be

    JSON (see limitations) • JSON Path • Ignoring fields 16
  35. JSON assert DSL (1) • Body is assumed to be

    JSON (see limitations) • JSON Path • Ignoring fields • Whitelisting 16
  36. JSON assert DSL (1) • Body is assumed to be

    JSON (see limitations) • JSON Path • Ignoring fields • Whitelisting • JSON Array 16
  37. When I get("/superheroes/Batman")
 
 And assert body.is(
 """
 {
 "name":

    "Batman",
 "realName": "Bruce Wayne",
 "city": "Gotham city",
 "hasSuperpowers": false,
 "publisher":{
 "name":"DC",
 "foundationYear":1934,
 "location":"Burbank, California"
 }
 }
 """
 ) 18
  38. Then assert body.path("hasSuperpowers").is(false) Then assert body.path("city").isPresent
 
 Then assert body.path("city").is("Gotham

    city")
 
 Then assert body.path("city").containsString("Gotham")
 
 Then assert body.path("city").matchesRegex(“.*city”.r)
 
 Then assert body.path("country").isAbsent
 19
  39. 
 Then assert body.path("publisher.name").is("DC")
 
 Then assert body.path("publisher.foundationYear").is(1934) Then assert

    body.path("publisher").ignoring("location").is(
 """
 {
 "name":"DC",
 "foundationYear":1934
 }
 """
 ) 20
  40. And assert body.ignoring("publisher.name", "publisher.location").is(
 """
 {
 "name": "Batman",
 "realName": "Bruce

    Wayne",
 "city": "Gotham city",
 "hasSuperpowers": false,
 "publisher":{
 "foundationYear":1934
 }
 }
 """
 )
 
 And assert body.whitelisting.is(
 """
 {
 "name": "Batman",
 "realName": "Bruce Wayne"
 }
 """
 ) 21
  41. Wrapper steps • Control the execution of a series of

    steps • Can be arbitrarily nested 22
  42. Wrapper steps • Control the execution of a series of

    steps • Can be arbitrarily nested trait WrapperStep extends Step {
 def nested: List[Step]
 }
 22
  43. 23

  44. Repeat(3) {
 When I get("http://superhero.io/batman")
 Then assert status.is(200)
 } RepeatDuring(300.millis)

    {
 When I get("http://superhero.io/batman")
 Then assert status.is(200)
 } 23
  45. Repeat(3) {
 When I get("http://superhero.io/batman")
 Then assert status.is(200)
 } RepeatDuring(300.millis)

    {
 When I get("http://superhero.io/batman")
 Then assert status.is(200)
 } RetryMax(3) {
 When I get("http://superhero.io/batman")
 Then assert status.is(200)
 } 23
  46. 24

  47. Eventually(maxDuration = 15.seconds, interval = 200.millis) {
 When I get(“http://superhero.io/random”)

    Then assert body.path("hasSuperpowers").is(true)
 } Concurrently(factor = 3, maxTime = 10 seconds) {
 When I get(“http://superhero.io/batman")
 Then assert status.is(200)
 } 24
  48. Eventually(maxDuration = 15.seconds, interval = 200.millis) {
 When I get(“http://superhero.io/random”)

    Then assert body.path("hasSuperpowers").is(true)
 } Concurrently(factor = 3, maxTime = 10 seconds) {
 When I get(“http://superhero.io/batman")
 Then assert status.is(200)
 } Within(maxDuration = 10 seconds) {
 When I get("http://superhero.io/batman")
 Then assert status.is(200)
 } 24
  49. 25

  50. WithHeaders(("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")){
 When I get("http://superhero.io/secured") 
 Then assert status.is(200)


    } WithBasicAuth("admin", "root"){
 When I get("http://superhero.io/secured") 
 Then assert status.is(200)
 } 25
  51. Placeholders • syntax to extract values from Session • HTTP

    effect request body, parameters, URL, headers 26
  52. Placeholders • syntax to extract values from Session • HTTP

    effect request body, parameters, URL, headers • JSON path, assertions 26
  53. Placeholders • syntax to extract values from Session • HTTP

    effect request body, parameters, URL, headers • JSON path, assertions • actually built-in…almost everywhere 26
  54. Placeholders • syntax to extract values from Session • HTTP

    effect request body, parameters, URL, headers • JSON path, assertions • actually built-in…almost everywhere • In your custom steps via the Resolver API manually 26
  55. Given I save("favorite-superhero" → "Batman")
 
 Then assert session_value("favorite-superhero").is("Batman")
 


    When I get("/superheroes/<favorite-superhero>")
 
 Then assert body.is(
 """
 {
 "name": "<favorite-superhero>",
 "realName": "Bruce Wayne",
 "city": "Gotham city",
 "publisher": "DC"
 }
 """
 ) 27
  56. • <random-uuid> for a random UUID • <random-positive-integer> for a

    random Integer • <random-string> for a random String of length 5 Built-in placeholders 28
  57. • <random-uuid> for a random UUID • <random-positive-integer> for a

    random Integer • <random-string> for a random String of length 5 • <random-alphanum-string> for a random alphanumeric Built-in placeholders 28
  58. • <random-uuid> for a random UUID • <random-positive-integer> for a

    random Integer • <random-string> for a random String of length 5 • <random-alphanum-string> for a random alphanumeric • <random-boolean> for a random Boolean string Built-in placeholders 28
  59. • <random-uuid> for a random UUID • <random-positive-integer> for a

    random Integer • <random-string> for a random String of length 5 • <random-alphanum-string> for a random alphanumeric • <random-boolean> for a random Boolean string • <timestamp> for the current timestamp Built-in placeholders 28
  60. 29

  61. post("/superheroes").withBody(
 """
 { "id": "<random-uuid>", 
 "name": "<random-string>", "age": <random-positive-integer>,

    "hasSuperpowers": <random-boolean> } """ ) Eventually(maxDuration = 500.hours, interval = 50.milliseconds) {
 When I get("/superheroes/<random-string>") Then assert status.is(200)
 } 29
  62. Data table • used to express JSON arrays • supports

    placeholders When I get("/superheroes") Then assert body.asArray.ignoringEach("publisher").is(
 """
 | name | realName | city | hasSuperpowers |
 | "Batman" | "Bruce Wayne" | "Gotham city" | false |
 | "Superman" | "Clark Kent" | "Metropolis" | true |
 | "Spiderman" | "Peter Parker" | "New York" | true |
 """
 ) 30
  63. DSL composition • Avoid copy paste • Reuse sub part

    of scenarios • Increases composition 32
  64. DSL composition • Avoid copy paste • Reuse sub part

    of scenarios • Increases composition • Any wrapper step 32
  65. DSL composition • Avoid copy paste • Reuse sub part

    of scenarios • Increases composition • Any wrapper step • “Attach" for regular step 32
  66. 33

  67. def superhero_exists(name: String) =
 Attach {
 When I get(s"/superheroes/$name")
 Then

    assert status.is(200)
 } Then assert superhero_exists("Batman") 33
  68. def superhero_exists(name: String) =
 Attach {
 When I get(s"/superheroes/$name")
 Then

    assert status.is(200)
 } def random_superheroes_until(name: String) =
 Eventually(maxDuration = 3 seconds, interval = 10 millis) {
 When I get("/superheroes/random")
 Then assert body.path("name").is(name)
 Then I print_step("bingo!")
 } Then assert superhero_exists("Batman") 33
  69. def superhero_exists(name: String) =
 Attach {
 When I get(s"/superheroes/$name")
 Then

    assert status.is(200)
 } def random_superheroes_until(name: String) =
 Eventually(maxDuration = 3 seconds, interval = 10 millis) {
 When I get("/superheroes/random")
 Then assert body.path("name").is(name)
 Then I print_step("bingo!")
 } Then assert random_superheroes_until("Batman") Then assert superhero_exists("Batman") 33
  70. HttpService • Abstracts HTTP calls to endpoint behind Effect Step

    • Increases step reusability • Useful to work against different endpoints 34
  71. HttpService • Abstracts HTTP calls to endpoint behind Effect Step

    • Increases step reusability • Useful to work against different endpoints • Offers helpers to work on response 34
  72. 35

  73. def create_publisher(body: String) = EffectStep(
 title = s"create the publisher\n

    $body",
 effect = http.requestEffect(
 request = HttpRequest.post("/publishers").withBody(body),
 extractor = RootExtractor("publisher"),
 expectedStatus = Some(201)
 )
 ) 35
  74. def create_publisher(body: String) = EffectStep(
 title = s"create the publisher\n

    $body",
 effect = http.requestEffect(
 request = HttpRequest.post("/publishers").withBody(body),
 extractor = RootExtractor("publisher"),
 expectedStatus = Some(201)
 )
 ) Given I create_publisher {
 """
 "name":"DC",
 "foundationYear":1934,
 "location":"Burbank, California"
 """
 } 35
  75. And more… • Hooks • GraphQL • Web hook testing

    via HTTP mocking • Server-Sent-Event 36
  76. And more… • Hooks • GraphQL • Web hook testing

    via HTTP mocking • Server-Sent-Event • Custom session extractors 36
  77. And more… • Hooks • GraphQL • Web hook testing

    via HTTP mocking • Server-Sent-Event • Custom session extractors • Automatic headers setting 36
  78. And more… • Hooks • GraphQL • Web hook testing

    via HTTP mocking • Server-Sent-Event • Custom session extractors • Automatic headers setting • Data table driven step 36
  79. And more… • Hooks • GraphQL • Web hook testing

    via HTTP mocking • Server-Sent-Event • Custom session extractors • Automatic headers setting • Data table driven step • … 36
  80. Main dependencies • Akka-http for the client • Circe for

    the JSON handling • Cats for the FP goodness 38
  81. Main dependencies • Akka-http for the client • Circe for

    the JSON handling • Cats for the FP goodness • Parboiled2 for parsing (placeholder, JSON path, DT) 38
  82. Main dependencies • Akka-http for the client • Circe for

    the JSON handling • Cats for the FP goodness • Parboiled2 for parsing (placeholder, JSON path, DT) • Scalatest for the SBT integration and test runner 38
  83. DSL internals (1) • Dynamic trait • Infix notation 39

    sealed trait Starters extends Dynamic {
 def name: String
 def applyDynamic(mandatoryWord: String)(step: Step) = step.setTitle(s"$name $mandatoryWord ${step.title}")
 }
 
 case object When extends Starters { val name = "When" }
  84. DSL internals (2) • How do HTTP requests are turned

    into steps? implicit def httpRequestToStep[A: Show: Resolvable: Encoder] (request: HttpRequest[A]): EffectStep =
 EffectStep(
 title = request.compactDescription,
 effect = http.requestEffect(request)
 ) 40
  85. DSL internals (3) • How is the Feature object built?

    • Macro collects elements of a given type 41
  86. DSL internals (3) • How is the Feature object built?

    • Macro collects elements of a given type • Contributed by @OlegIlyenko 41
  87. DSL internals (3) • How is the Feature object built?

    • Macro collects elements of a given type • Contributed by @OlegIlyenko case class BodyElementCollector[Body, Result](fn: List[Body] ⇒ Result) {
 def apply(body: ⇒ Body): Result = macro Macro.collectImpl
 } 41
  88. 42

  89. def Feature(name: String, ignored: Boolean = false) =
 BodyElementCollector[ScenarioDef, FeatureDef]{scenarios

    ⇒ FeatureDef(name, scenarios, ignored) } def Scenario(name: String, ignored: Boolean = false) =
 BodyElementCollector[Step, ScenarioDef]{steps ⇒ ScenarioDef(name, steps, ignored) } 42
  90. def Feature(name: String, ignored: Boolean = false) =
 BodyElementCollector[ScenarioDef, FeatureDef]{scenarios

    ⇒ FeatureDef(name, scenarios, ignored) } def Scenario(name: String, ignored: Boolean = false) =
 BodyElementCollector[Step, ScenarioDef]{steps ⇒ ScenarioDef(name, steps, ignored) } def Concurrently(factor: Int, maxTime: FiniteDuration) =
 BodyElementCollector[Step, Step] { steps ⇒
 ConcurrentlyStep(steps, factor, maxTime)
 } 42
  91. Conceptual execution flow • Macro collects steps to build the

    scenario • Macro collects scenarios to build the feature 43
  92. Conceptual execution flow • Macro collects steps to build the

    scenario • Macro collects scenarios to build the feature • Feature is turned into a Scalatest AsyncWordSpec 43
  93. Conceptual execution flow • Macro collects steps to build the

    scenario • Macro collects scenarios to build the feature • Feature is turned into a Scalatest AsyncWordSpec • Each scenario becomes a test 43
  94. Conceptual execution flow • Macro collects steps to build the

    scenario • Macro collects scenarios to build the feature • Feature is turned into a Scalatest AsyncWordSpec • Each scenario becomes a test • Scalatest invokes the engine that runs the scenario 43
  95. Conceptual execution flow • Macro collects steps to build the

    scenario • Macro collects scenarios to build the feature • Feature is turned into a Scalatest AsyncWordSpec • Each scenario becomes a test • Scalatest invokes the engine that runs the scenario • Result of the execution is fed into a Scalatest assert() 43
  96. Teamcity integration • Runs against different environments • Injects configuration

    via typesafe-config arguments • sbt test (testQuick to fight flaky systems) 46
  97. Teamcity integration • Runs against different environments • Injects configuration

    via typesafe-config arguments • sbt test (testQuick to fight flaky systems) • Or build uber-jar test artifact to avoid recompilation 46
  98. Features organisation • Sub-project - commit tests & features in

    a single PR • Abstract CommercetoolsFeature with the usual config 48
  99. Features organisation • Sub-project - commit tests & features in

    a single PR • Abstract CommercetoolsFeature with the usual config • One package per entity/resource 48
  100. Features organisation • Sub-project - commit tests & features in

    a single PR • Abstract CommercetoolsFeature with the usual config • One package per entity/resource • Custom steps in traits 48
  101. Usage outside of commercetools • Used by three others devs

    (Gitter discussions) • Scale unknown, might be toy projects 50
  102. Usage outside of commercetools • Used by three others devs

    (Gitter discussions) • Scale unknown, might be toy projects • Maybe more according to the maven DL stats 50
  103. Usage outside of commercetools • Used by three others devs

    (Gitter discussions) • Scale unknown, might be toy projects • Maybe more according to the maven DL stats • ¯\_(ツ)_/¯ 50
  104. Limitations (1) • JSON only (for now) • Moving away

    from the well-known Gherkin syntax 51
  105. Limitations (1) • JSON only (for now) • Moving away

    from the well-known Gherkin syntax • Important “classpath” (acceptance testing) 51
  106. Limitations (1) • JSON only (for now) • Moving away

    from the well-known Gherkin syntax • Important “classpath” (acceptance testing) • No beforeAllFeatures and afterAllFeatures 51
  107. Limitations (1) • JSON only (for now) • Moving away

    from the well-known Gherkin syntax • Important “classpath” (acceptance testing) • No beforeAllFeatures and afterAllFeatures • Obviously designed to fit our use case in first place 51
  108. Limitations (2) • Managing authentication headers input can be tricky

    • Full documentation on a Readme file • Rely partly on exception internally (0.11 will hopefully fix it) 52
  109. Limitations (2) • Managing authentication headers input can be tricky

    • Full documentation on a Readme file • Rely partly on exception internally (0.11 will hopefully fix it) • Truck factor of 1 52
  110. Limitations (2) • Managing authentication headers input can be tricky

    • Full documentation on a Readme file • Rely partly on exception internally (0.11 will hopefully fix it) • Truck factor of 1 • Makes acceptance tests so fun that you less write UT ;) 52
  111. Road to 1.0 • Hopefully in 2017! • Minimise breaking

    changes in the DSL • Infrastructure - change underlying test runner 53
  112. Road to 1.0 • Hopefully in 2017! • Minimise breaking

    changes in the DSL • Infrastructure - change underlying test runner • Features - introduce matchers 53
  113. Road to 1.0 • Hopefully in 2017! • Minimise breaking

    changes in the DSL • Infrastructure - change underlying test runner • Features - introduce matchers • Docs - compiled example on a website 53
  114. Closing notes • You learned what the tool can do

    and its limitations • The built-in DSL takes you a long way 54
  115. Closing notes • You learned what the tool can do

    and its limitations • The built-in DSL takes you a long way • Scala is great for building DSLs 54
  116. Closing notes • You learned what the tool can do

    and its limitations • The built-in DSL takes you a long way • Scala is great for building DSLs • Focus on solving problems you actually have 54