128
REST Test Exploring DSL design in Scala

REST Test - Exploring DSL design in Scala

Embed Size (px)

DESCRIPTION

A step by step guide to creating a DSL to test rest web services. I presented this talk to the Scala Days in Berlin the 18th of June 2014 http://www.scaladays.org/#schedule/RESTTest--exploring-DSL-design-in-Scala Watch this presentation online https://www.parleys.com/play/53a7d2d1e4b0543940d9e56c

Citation preview

Page 1: REST Test - Exploring DSL design in Scala

REST Test

Exploring DSL design in Scala

Page 2: REST Test - Exploring DSL design in Scala

Who am I?

Iain Hull

Email: [email protected]: @IainHullWeb: http://IainHull.github.io

Workday

Page 3: REST Test - Exploring DSL design in Scala

HTTP Clients … What’s the problem?http://www.hdwpapers.com/pretty_face_of_boxer_dog_wallpaper_hd-wallpapers.html

Page 4: REST Test - Exploring DSL design in Scala

Simple HTTP Clientcase class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String])

case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String])

type HttpClient = Request => Response

Page 5: REST Test - Exploring DSL design in Scala

Simple HTTP Clientcase class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String])

case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String])

type HttpClient = Request => Response

Page 6: REST Test - Exploring DSL design in Scala

Simple HTTP Clientcase class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String])

case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String])

type HttpClient = Request => Response

Page 7: REST Test - Exploring DSL design in Scala

Simple HTTP Clientcase class Request( method: Method, url: URI, headers: Map[String, List[String]], body: Option[String])

case class Response( statusCode: Int, headers: Map[String, List[String]], body: Option[String])

type HttpClient = Request => Response

Page 8: REST Test - Exploring DSL design in Scala

Using the Clientval request = Request( GET, new URI("http://api.rest.org/person", Map(), None))

val response = httpClient(request)

assert(response.code1 === Status.OK)assert(jsonAsList[Person](response.body.get) === EmptyList)

Page 9: REST Test - Exploring DSL design in Scala

Using the Clientval request = Request( GET, new URI("http://api.rest.org/person", Map(), None))

val response = httpClient(request)

assert(response.code === Status.OK)assert(jsonAsList[Person](response.body.get) === EmptyList)

Page 10: REST Test - Exploring DSL design in Scala

Using the Clientval request = Request( GET, new URI("http://api.rest.org/person", Map(), None))

val response = httpClient(request)

assert(response.code === Status.OK)assert(jsonAsList[Person](response.body.get) === EmptyList)

Page 11: REST Test - Exploring DSL design in Scala

Using the Clientval request = Request( GET, new URI("http://api.rest.org/person", Map(), None))

val response = httpClient(request)

assert(response.code === Status.OK)assert(jsonAsList[Person](response.body.get) === EmptyList)

Page 12: REST Test - Exploring DSL design in Scala

Sample API

GET /person List all the persons

POST /person Create a new person

GET /person/{id} Retrieve a person

DELETE /person/{id} Delete a person

Page 13: REST Test - Exploring DSL design in Scala

Retrieve the list

GET /person Verify list is empty

Page 14: REST Test - Exploring DSL design in Scala

Create a new person

GET /person Verify list is empty

POST /person Verify it succeeds and returns an id

Page 15: REST Test - Exploring DSL design in Scala

Retrieve the person

GET /person Verify list is empty

POST /person Verify it succeeds and returns an id

GET /person/{id} Verify details are the same

Page 16: REST Test - Exploring DSL design in Scala

Retrieve the list

GET /person Verify list is empty

POST /person Verify it succeeds and returns an id

GET /person/{id} Verify details are the same

GET /person Verify list contains the person

Page 17: REST Test - Exploring DSL design in Scala

Delete the person

GET /person Verify list is empty

POST /person Verify it succeeds and returns an id

GET /person/{id} Verify details are the same

GET /person Verify list contains the person

DELETE /person/{id} Verify the status code

Page 18: REST Test - Exploring DSL design in Scala

Retrieve the list

GET /person Verify list is empty

POST /person Verify it succeeds and returns an id

GET /person/{id} Verify details are the same

GET /person Verify list contains the person

DELETE /person/{id} Verify the status code

GET /person Verify list is empty

Page 19: REST Test - Exploring DSL design in Scala

val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))

Page 20: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").head

Page 21: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))

Page 22: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))

Page 23: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))

Page 24: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))

Page 25: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""

val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r1.statusCode === Status.OK)r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))assert(r2.statusCode === Status.Created)val id = r2.headers("X-Person-Id").head

val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r3.statusCode === Status.OK)r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body"))}

val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r4.statusCode === Status.OK)r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body"))}

val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r5.statusCode === Status.OK)

val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r6.statusCode === Status.OK)r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

Page 26: REST Test - Exploring DSL design in Scala

Agh!! I can’t read

this

Page 27: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""

val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r1.statusCode === Status.OK)r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))assert(r2.statusCode === Status.Created)val id = r2.headers("X-Person-Id").head

val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r3.statusCode === Status.OK)r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body"))}

val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r4.statusCode === Status.OK)r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body"))}

val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r5.statusCode === Status.OK)

val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r6.statusCode === Status.OK)r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

Page 28: REST Test - Exploring DSL design in Scala

New CodeModify-ing Code

Reading Code

http://www.codinghorror.com/blog/2006/09/when-understanding-means-rewriting.html

Page 29: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/paulwicks/1753350803/sizes/o/

Boilerplate

Page 30: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/paulwicks/1753350803/sizes/o/

Boilerplate

Page 31: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/whoshotya/1014730135/

Domain Specific Language

Page 32: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/whoshotya/1014730135/

Domain Specific Language

How can it help?

Page 33: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/whoshotya/1014730135/

Domain Specific Language

How can it help?

How do I write one?

Page 34: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/whoshotya/1014730135/

Domain Specific Language

How can it help?

How do I write one?

Scala

Page 35: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))

Page 36: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))

Page 37: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))

Page 38: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))val id = r2.headers("X-Person-Id").headval r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))

Page 39: REST Test - Exploring DSL design in Scala
Page 40: REST Test - Exploring DSL design in Scala

Applying the builderval personJson = """{ "name": "Jason" }"""val rb = RequestBuilder() .withUrl("http://api.rest.org/person/")val r1 = httpClient(rb.withMethod(GET).toRequest)val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest)val id = r2.headers("X-Person-Id").headval r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest)val r4 = httpClient(rb.withMethod(GET).toRequest)val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest)val r6 = httpClient(rb.withMethod(GET).toRequest)

Page 41: REST Test - Exploring DSL design in Scala

Applying the builderval personJson = """{ "name": "Jason" }"""val rb = RequestBuilder() .withUrl("http://api.rest.org/person/")val r1 = httpClient(rb.withMethod(GET).toRequest)val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest)val id = r2.headers("X-Person-Id").headval r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest)val r4 = httpClient(rb.withMethod(GET).toRequest)val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest)val r6 = httpClient(rb.withMethod(GET).toRequest)

Page 42: REST Test - Exploring DSL design in Scala

Applying the builderval personJson = """{ "name": "Jason" }"""val rb = RequestBuilder() .withUrl("http://api.rest.org/person/")val r1 = httpClient(rb.withMethod(GET).toRequest)val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest)val id = r2.headers("X-Person-Id").headval r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest)val r4 = httpClient(rb.withMethod(GET).toRequest)val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest)val r6 = httpClient(rb.withMethod(GET).toRequest)

Page 43: REST Test - Exploring DSL design in Scala

Applying the builderval personJson = """{ "name": "Jason" }"""val rb = RequestBuilder() .withUrl("http://api.rest.org/person/")val r1 = httpClient(rb.withMethod(GET).toRequest)val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest)val id = r2.headers("X-Person-Id").headval r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest)val r4 = httpClient(rb.withMethod(GET).toRequest)val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest)val r6 = httpClient(rb.withMethod(GET).toRequest)

Page 44: REST Test - Exploring DSL design in Scala

Implicit Conversionval personJson = """{ "name": "Jason" }"""val rb = RequestBuilder() .withUrl("http://api.rest.org/person/")val r1 = httpClient(rb.withMethod(GET).toRequest)val r2 = httpClient(rb.withMethod(POST) .withBody(personJson).toRequest)val id = r2.headers("X-Person-Id").headval r3 = httpClient(rb.withMethod(GET).addPath(id) .toRequest)val r4 = httpClient(rb.withMethod(GET).toRequest)val r5 = httpClient(rb.withMethod(DELETE).addPath(id) .toRequest)val r6 = httpClient(rb.withMethod(GET).toRequest)

implicit def toRequest(b: RequestBuilder): Request = b.toRequest

Page 45: REST Test - Exploring DSL design in Scala

Implicit Conversionval personJson = """{ "name": "Jason" }"""val rb = RequestBuilder() .withUrl("http://api.rest.org/person/"val r1 = httpClient(rb.withMethod(GET))val r2 = httpClient(rb.withMethod(POST) .withBody(personJson))val id = r2.headers("X-Person-Id").headval r3 = httpClient(rb.withMethod(GET).addPath(id))

val r4 = httpClient(rb.withMethod(GET))val r5 = httpClient(rb.withMethod(DELETE).addPath(id))

val r6 = httpClient(rb.withMethod(GET))

implicit def toRequest(b: RequestBuilder): Request = b.toRequest

Page 46: REST Test - Exploring DSL design in Scala

Implicit Conversionval personJson = """{ "name": "Jason" }"""val rb = RequestBuilder() .withUrl("http://api.rest.org/person/")val r1 = httpClient(rb.withMethod(GET))val r2 = httpClient(rb.withMethod(POST) .withBody(personJson))val id = r2.headers("X-Person-Id").headval r3 = httpClient(rb.withMethod(GET).addPath(id))val r4 = httpClient(rb.withMethod(GET))val r5 = httpClient(rb.withMethod(DELETE).addPath(id))val r6 = httpClient(rb.withMethod(GET))

Page 47: REST Test - Exploring DSL design in Scala

Nice, but show me some DSL

Page 48: REST Test - Exploring DSL design in Scala

Narrate your code

• Get from url http://api.rest.org/person/

• Post personJson to url http://api.rest.org/person/

• Get from url http://api.rest.org/person/personId

• Delete from url http://api.rest.org/person/personId

Page 49: REST Test - Exploring DSL design in Scala

Narrate your code

• Get from url http://api.rest.org/person/

• Post personJson to url http://api.rest.org/person/

• Get from url http://api.rest.org/person/personId

• Delete from url http://api.rest.org/person/personId

Page 50: REST Test - Exploring DSL design in Scala

Bootstapping the DSLhttpClient(RequestBuilder() .withMethod(GET) .withUrl("http://api.rest.org/person/"))

Page 51: REST Test - Exploring DSL design in Scala

Bootstapping the DSLhttpClient(RequestBuilder() .withMethod(GET) .withUrl("http://api.rest.org/person/"))

Page 52: REST Test - Exploring DSL design in Scala

Bootstapping the DSLhttpClient(RequestBuilder() .withMethod(GET) .withUrl("http://api.rest.org/person/"))

implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)

Page 53: REST Test - Exploring DSL design in Scala

Bootstapping the DSLhttpClient(GET.withUrl("http://api.rest.org/person/"))

implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)

Page 54: REST Test - Exploring DSL design in Scala

Bootstapping the DSLhttpClient(GET.withUrl("http://api.rest.org/person/"))

implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)

methodToRequestBuilder(GET) .withUrl("http://api.rest.org/person/")

Page 55: REST Test - Exploring DSL design in Scala

Bootstapping the DSLhttpClient(GET.withUrl("http://api.rest.org/person/"))

implicit def methodToRequestBuilder(method: Method): RequestBuilder = RequestBuilder().withMethod(method)

Page 56: REST Test - Exploring DSL design in Scala

Bootstapping the DSLhttpClient(GET withUrl "http://api.rest.org/person/")

Page 57: REST Test - Exploring DSL design in Scala

Initial DSLval personJson = """{ "name": "Jason" }"""val r1 = httpClient( GET withUrl "http://api.rest.org/person/")val r2 = httpClient( POST withUrl "http://api.rest.org/person/" withBody personJson)val id = r2.headers.get("X-Person-Id").get.headval r3 = httpClient( GET withUrl "http://api.rest.org/person/" addPath id)val r4 = httpClient( GET withUrl "http://api.rest.org/person/")val r5 = httpClient( DELETE withUrl "http://api.rest.org/person/" addPath id)val r6 = httpClient( GET withUrl "http://api.rest.org/person/")

Page 58: REST Test - Exploring DSL design in Scala

Initial DSLval personJson = """{ "name": "Jason" }"""val r1 = httpClient( GET withUrl "http://api.rest.org/person/")val r2 = httpClient( POST withUrl "http://api.rest.org/person/" withBody personJson)val id = r2.headers.get("X-Person-Id").get.headval r3 = httpClient( GET withUrl "http://api.rest.org/person/" addPath id)val r4 = httpClient( GET withUrl "http://api.rest.org/person/")val r5 = httpClient( DELETE withUrl "http://api.rest.org/person/" addPath id)val r6 = httpClient( GET withUrl "http://api.rest.org/person/")

Page 59: REST Test - Exploring DSL design in Scala

Initial DSL

val personJson = """{ "name": "Jason" }"""val r1 = httpClient(GET withUrl "http://api.rest.org/person/")val r2 = httpClient(POST withUrl "http://api.rest.org/person/" withBody personJson)val id = r2.headers.get("X-Person-Id").get.headval r3 = httpClient(GET withUrl "http://api.rest.org/person/" addPath id)val r4 = httpClient(GET withUrl "http://api.rest.org/person/")val r5 = httpClient(DELETE withUrl "http://api.rest.org/person/" addPath id)

implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) }}

Page 60: REST Test - Exploring DSL design in Scala

Initial DSLimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""val r1 = GET withUrl "http://api.rest.org/person/" execute ()val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute ()val id = r2.headers.get("X-Person-Id").get.head val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()val r4 = GET withUrl "http://api.rest.org/person/" execute ()val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()val r6 = GET withUrl "http://api.rest.org/person/" execute ()

implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) }}

Page 61: REST Test - Exploring DSL design in Scala

Initial DSLimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""val r1 = GET withUrl "http://api.rest.org/person/" execute ()val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute ()val id = r2.headers.get("X-Person-Id").get.headval r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()val r4 = GET withUrl "http://api.rest.org/person/" execute ()val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()val r6 = GET withUrl "http://api.rest.org/person/" execute ()

implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) }}

Page 62: REST Test - Exploring DSL design in Scala

Initial DSLimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""val r1 = GET withUrl "http://api.rest.org/person/" execute ()val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute ()val id = r2.headers.get("X-Person-Id").get.headval r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()val r4 = GET withUrl "http://api.rest.org/person/" execute ()val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()val r6 = GET withUrl "http://api.rest.org/person/" execute ()

implicit class RichRequestBuilder(builder: RequestBuilder) { def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) }}

Page 63: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }""”

val r1 = GET withUrl "http://api.rest.org/person/" execute ()val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute ()val id = r2.headers.get("X-Person-Id”)val r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()val r4 = GET withUrl "http://api.rest.org/person/" execute ()val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()val r6 = GET withUrl "http://api.rest.org/person/" execute ()

Page 64: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

val r1 = GET withUrl "http://api.rest.org/person/" execute ()val r2 = POST withUrl "http://api.rest.org/person/" withBody personJson execute ()val id = r2.headers.get("X-Person-Id").get.headval r3 = GET withUrl "http://api.rest.org/person/" addPath id execute ()val r4 = GET withUrl "http://api.rest.org/person/" execute ()val r5 = DELETE withUrl "http://api.rest.org/person/" addPath id execute ()val r6 = GET withUrl "http://api.rest.org/person/" execute ()

implicit def methodToRequestBuilder(m: Method): RequestBuilder = RequestBuilder().withMethod(m)

Page 65: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...implicit val rb = RequestBuilder() withUrl "http://api.rest.org/person/"

val personJson = """{ "name": "Jason" }"""

val r1 = GET execute ()val r2 = POST withBody personJson execute ()val id = r2.headers.get("X-Person-Id").get.headval r3 = GET addPath id execute ()val r4 = GET execute ()val r5 = DELETE addPath id execute ()val r6 = GET execute ()

implicit def methodToRequestBuilder(m: Method) (implicit builder: RequestBuilder): RequestBuilder = builder.withMethod(m)

Page 66: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...implicit val rb = RequestBuilder() withUrl "http://api.rest.org/person/"

val personJson = """{ "name": "Jason" }"""

val r1 = GET execute ()val r2 = POST withBody personJson execute ()val id = r2.headers.get("X-Person-Id").get.headval r3 = GET addPath id execute ()val r4 = GET execute ()val r5 = DELETE addPath id execute ()val r6 = GET execute ()

Page 67: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

using(_ withUrl "http://api.rest.org/person") { implicit rb =>

val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute ()}

Page 68: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

using(_ withUrl "http://api.rest.org/person") { implicit rb =>

val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute ()}

def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder))}

Page 69: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

using(_ withUrl "http://api.rest.org/person") { implicit rb =>

val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute ()}

def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder))}

Page 70: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

using(_ withUrl "http://api.rest.org/person") { implicit rb =>

val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute ()}

def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder))}

Page 71: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

using(_ withUrl "http://api.rest.org/person") { implicit rb =>

val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute ()}

def using(config: RequestBuilder => RequestBuilder) (process: RequestBuilder => Unit) (implicit builder: RequestBuilder): Unit = { process(config(builder))}

Page 72: REST Test - Exploring DSL design in Scala

Common Configurationobject RequestBuilder { implicit val emptyBuilder = RequestBuilder( None, None, Seq(), Seq(), None)}

Page 73: REST Test - Exploring DSL design in Scala

Common Configurationimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

using(_ withUrl "http://api.rest.org/person") { implicit rb =>

val r1 = GET execute () val r2 = POST withBody personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET addPath id execute () val r4 = GET execute () val r5 = DELETE addPath id execute () val r6 = GET execute ()}

Page 74: REST Test - Exploring DSL design in Scala

A spoonful of sugar

Page 75: REST Test - Exploring DSL design in Scala

A Spoonful of sugarimplicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString)

def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*)

// ...}

Page 76: REST Test - Exploring DSL design in Scala

A Spoonful of sugarimplicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString)

def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*)

// ...}

Page 77: REST Test - Exploring DSL design in Scala

A Spoonful of sugarimplicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString)

def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*)

// ...}

Page 78: REST Test - Exploring DSL design in Scala

A Spoonful of sugarimplicit class RichRequestBuilder(b: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString)

def :?(params: (Symbol, Any)*) = b.addQuery(params map ( p => (p._1.name, p._2.toString)): _*)

// ...}

Page 79: REST Test - Exploring DSL design in Scala

A Spoonful of sugarimplicit val httpClient = ...

val personJson = """{ "name": "Jason" }"""

using(_ url "http://api.rest.org") { implicit rb =>

val r1 = GET / "person" execute () val r2 = POST / "person" body personJson execute () val id = r2.headers.get("X-Person-Id").get.head val r3 = GET / "person" / id execute () val r4 = GET / "person" execute () val r5 = DELETE / "person" / id execute () val r6 = GET / "person" :? ('page -> 2, 'per_page -> 100) execute ()}

Page 80: REST Test - Exploring DSL design in Scala

Extracting values

val r1 = GET url "http://api.rest.org/person/" execute ()val code1 = r1.statusCodeval list1 = jsonToList[Person](Json.parse(r1.body), JsPath)

Page 81: REST Test - Exploring DSL design in Scala

Extracting values

val r1 = GET url "http://api.rest.org/person/" execute ()val code1 = r1.statusCodeval list1 = jsonToList[Person](Json.parse(r1.body), JsPath)

Get from url http://api.rest.org/person/ returning the status code and the body as list of persons

Page 82: REST Test - Exploring DSL design in Scala

Extracting values

val r1 = GET url "http://api.rest.org/person" execute ()val code1 = r1.statusCodeval list1 = jsonToValue[Person](Json.parse(r1.body), JsPath)

val code1 = GET url "http://api.rest.org/person" returning (StatusCode)

Page 83: REST Test - Exploring DSL design in Scala

Extracting values

val r1 = GET url "http://api.rest.org/person" execute ()val code1 = r1.statusCodeval list1 = jsonToValue[Person](Json.parse(r1.body), JsPath)

val (code1, list1) = GET url "http://api.rest.org/person" returning (StatusCode, BodyAsPersonList)

Page 84: REST Test - Exploring DSL design in Scala

Extractors

Page 85: REST Test - Exploring DSL design in Scala

Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}

Page 86: REST Test - Exploring DSL design in Scala

Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}

Page 87: REST Test - Exploring DSL design in Scala

Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}

Page 88: REST Test - Exploring DSL design in Scala

Extracting valuestrait ExtractorLike[+A] { def name: String def value(res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption}

Page 89: REST Test - Exploring DSL design in Scala

Extracting valuescase class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } }

def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp)

def as(newName: String) = copy(name = newName)}

Page 90: REST Test - Exploring DSL design in Scala

Extracting valuescase class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } }

def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp)

def as(newName: String) = copy(name = newName)}

Page 91: REST Test - Exploring DSL design in Scala

Extracting valuescase class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } }

def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp)

def as(newName: String) = copy(name = newName)}

Page 92: REST Test - Exploring DSL design in Scala

Extracting valuescase class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException( s"Cannot extract $name from Response: $e", e)) } }

def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp)

def as(newName: String) = copy(name = newName)}

Page 93: REST Test - Exploring DSL design in Scala

Extracting valuesval StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode)

Page 94: REST Test - Exploring DSL design in Scala

Extracting valuesval StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode)

val BodyText = Extractor[String]( "BodyText", r => r.body.get)

Page 95: REST Test - Exploring DSL design in Scala

Extracting valuesval StatusCode = Extractor[Int]( "StatusCode", r => r.statusCode)

val BodyText = Extractor[String]( "BodyText", r => r.body.get)

def header(name: String) = { Extractor[String](s"header($name)", r => r.headers(name).mkString(", "))}

Page 96: REST Test - Exploring DSL design in Scala

Extractors Composeval JsonBody = BodyText andThen Json.parse as "JsonBody"

Page 97: REST Test - Exploring DSL design in Scala

Extractors Composeval JsonBody = BodyText andThen Json.parse as "JsonBody"

def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]]

JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]")}

Page 98: REST Test - Exploring DSL design in Scala

Extractors Composeval JsonBody = BodyText andThen Json.parse as "JsonBody"

def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]]

JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]")}

def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] = jsonBodyAs(JsPath)

Page 99: REST Test - Exploring DSL design in Scala

Extractors Composeval JsonBody = BodyText andThen Json.parse as "JsonBody”

def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]]

JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]")}

def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] = jsonBodyAs(JsPath)

val BodyAsPerson = jsonBodyAs[Person]val BodyAsName = jsonBodyAs[String](__ \ "name")

Page 100: REST Test - Exploring DSL design in Scala

Extracting valuesimplicit class RichRequestBuilder(builder: RequestBuilder) {

// ...

def returning[T1](ext1: Extractor[T1]) (implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) }

def returning[T1, T2](ext1: Extractor[T1], ext1: Extractor[T2]) (implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) }}

Page 101: REST Test - Exploring DSL design in Scala

Extracting valuesimplicit class RichRequestBuilder(builder: RequestBuilder) {

// ...

def returning[T1](ext1: Extractor[T1]) (implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) }

def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2]) (implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) }}

Page 102: REST Test - Exploring DSL design in Scala

Extracting valuesimplicit class RichRequestBuilder(builder: RequestBuilder) {

// ...

def returning[T1](ext1: Extractor[T1]) (implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) }

def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2]) (implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) }}

Page 103: REST Test - Exploring DSL design in Scala

Extracting valuesusing(_ url "http://api.rest.org") { implicit rb =>

val (code1, list1) = GET / "person" returning (StatusCode, BodyAsPersonList) val (code2, id) = POST / "person" body personJson returning (StatusCode, header("X-Person-Id”) val (code3, person) = GET / "person" / id returning (StatusCode, BodyAsPerson) val (code4, list2) = GET / "person" returning (StatusCode, BodyAsPersonList) val code5 = DELETE / "person" / id returning StatusCode val (code6, list3) = GET / "person" returning (StatusCode, BodyAsPersonList)}

Page 104: REST Test - Exploring DSL design in Scala

Asserting values

val (code1, list1) = GET / "person" returning ( StatusCode, BodyAsPersonList)

assert (code1 === Status.OK)assert (list1 === EmptyList)

Page 105: REST Test - Exploring DSL design in Scala

Asserting values

val (code1, list1) = GET / "person" returning ( StatusCode, BodyAsPersonList)

assert (code1 === Status.OK)assert (list1 === EmptyList)

Get from url http://api.rest.org/person/ asserting the status code is OK and and the body is an empty list of persons

Page 106: REST Test - Exploring DSL design in Scala

Asserting values

val (code1, list1) = GET / "person" returning ( StatusCode, BodyAsPersonList)

assert (code1 === Status.OK)assert (list1 === EmptyList)

GET / "person" asserting ( StatusCode === Status.OK, BodyAsPersonList === EmptyList)

Page 107: REST Test - Exploring DSL design in Scala

Assertions

Page 108: REST Test - Exploring DSL design in Scala

Asserting values

GET / "person" asserting ( StatusCode === Status.OK, BodyAsPersonList === EmptyList)

type Assertion = Response => Option[String]

Page 109: REST Test - Exploring DSL design in Scala

Asserting valuesimplicit class RichExtractor[A](ext: ExtractorLike[A]) {

def ===[B >: A](expected: B): Assertion = { res => val maybeValue = ext.value(res) maybeValue match { case Success(value) if (value == expected) => None case Success(value) => Some(s"${ext.name}: $value != $expected") case Failure(e) => Some(e.getMessage) } }}

Page 110: REST Test - Exploring DSL design in Scala

Asserting valuesimplicit class RichExtractor[A](ext: ExtractorLike[A]) { def ===[B >: A](expected: B): Assertion = ??? def !==[B >: A](expected: B): Assertion = ???

def < [B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? def <=[B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? def > [B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? def >=[B >: A](expected: B) (implicit ord: math.Ordering[B]): Assertion = ??? }

Page 111: REST Test - Exploring DSL design in Scala

Asserting valuesimplicit class RichRequestBuilder(builder: RequestBuilder) {

def asserting(assertions: Assertion*) (implicit client: HttpClient): Response = { val response = execute() val failures = for { assertion <- assertions failureMessage <- assertion(response) } yield failureMessage if (failures.nonEmpty) { throw assertionFailed(failures) } response }}

Page 112: REST Test - Exploring DSL design in Scala

Asserting valuesusing(_ url "http://api.rest.org") { implicit rb =>

GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPerson === Jason) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList)}

Page 113: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/ajkohn2001/2532935194/

Wow I can read it!

Page 114: REST Test - Exploring DSL design in Scala

Asserting valuesusing(_ url "http://api.rest.org") { implicit rb =>

GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPerson === Jason) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList)}

Page 115: REST Test - Exploring DSL design in Scala

RTFM? Wheres TFM?

http://wallpaperscraft.com/download/dog_boxer_laptop_lie_face_52801/

Page 116: REST Test - Exploring DSL design in Scala

Codebase Structure

API

DSL

Page 117: REST Test - Exploring DSL design in Scala

Codebase Structure

API

DSL

Extractors

Page 118: REST Test - Exploring DSL design in Scala

Codebase Structure

API

DSL

Extractors

JsonExtractors

XmlExtractors

Page 119: REST Test - Exploring DSL design in Scala

Codebase Structure

API

DSL

Extractors

JsonExtractors

XmlExtractors

ExtendedDSL

ExtendedExtractors

Page 120: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""

val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r1.statusCode === Status.OK)r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))assert(r2.statusCode === Status.Created)val id = r2.headers("X-Person-Id").head

val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r3.statusCode === Status.OK)r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body"))}

val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r4.statusCode === Status.OK)r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body"))}

val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r5.statusCode === Status.OK)

val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r6.statusCode === Status.OK)r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

using(_ url "http://api.rest.org") { implicit rb =>

GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPersonList === Jason) GET asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / id asserting (StatusCode === Status.NotFound) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList)}

Boile

rpla

te T

est

DSL

Test

Page 121: REST Test - Exploring DSL design in Scala

val personJson = """{ "name": "Jason" }"""

val r1 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r1.statusCode === Status.OK)r1.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

val r2 = httpClient(Request( POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson)))assert(r2.statusCode === Status.Created)val id = r2.headers("X-Person-Id").head

val r3 = httpClient(Request( GET, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r3.statusCode === Status.OK)r3.body match { Some(body) => assert(jsonAs[Person](body) === Jason) None => fail("Expected a body"))}

val r4 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r4.statusCode === Status.OK)r4.body match { Some(body) => assert(jsonAsList[Person](body) === Seq(Jason)) None => fail("Expected a body"))}

val r5 = httpClient(Request( DELETE, new URI("http://api.rest.org/person/" + id), Map(), None))assert(r5.statusCode === Status.OK)

val r6 = httpClient(Request( GET, new URI("http://api.rest.org/person/"), Map(), None))assert(r6.statusCode === Status.OK)r6.body match { Some(body) => assert(jsonAsList[Person](body) === EmptyList) None => fail("Expected a body"))}

using(_ url "http://api.rest.org") { implicit rb =>

GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPersonList === Jason) GET asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / id asserting (StatusCode === Status.NotFound) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList)}

object Extractors { trait ExtractorLike[+A] { def name: String def value(implicit res: Response): Try[A] def unapply(res: Response): Option[A] = value(res).toOption } case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { override def value(implicit res: Response): Try[A] = { Try { op(res) } recoverWith { case e => Failure[A](new ExtractorFailedException(s"Cannot extract $name from Response: $e",e)) } } def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) def as(newName: String) = copy(name = newName) } val StatusCode = Extractor[Int]("StatusCode", r => r.statusCode) val BodyText = Extractor[String]("BodyText", r => r.body.get) def header(name: String) = { Extractor[String](s"header($name)", r => r.headers(name).mkString(", ")) } val JsonBody = BodyText andThen Json.parse as "JsonBody" def jsonBodyAs[T : Reads : ClassTag](path: JsPath): Extractor[T] = { val tag = implicitly[ClassTag[T]] JsonBody andThen (jsonToValue(_, path)) as (s"jsonBodyAs[${tag.runtimeClass.getSimpleName}]") } def jsonBodyAs[T : Reads : ClassTag]: Extractor[T] = jsonBodyAs(JsPath) val BodyAsPerson = jsonBodyAs[Person]}trait Dsl extends Api with Extractors { implicit def toRequest(b: RequestBuilder): Request = b.toRequest implicit def methodToRequestBuilder(m: Method)(implicit builder: RequestBuilder): RequestBuilder = builder.withMethod(m) def using(config: RequestBuilder => RequestBuilder)(process: RequestBuilder => Unit)(implicit builder: RequestBuilder): Unit = { process(config(builder)) }

type Assertion = Response => Option[String] implicit class RichRequestBuilder(builder: RequestBuilder) { def url(u: String) = b.withUrl(u) def body(b: String) = b.withBody(b) def /(p: Any) = b.addPath(p.toString) def :?(params: (Symbol, Any)*) = b.addQuery(params map (p => (p._1.name, p._2.toString)): _*) def execute()(implicit httpClient: HttpClient): Response = { httpClient(builder) } def returning[T1](ext1: Extractor[T1])(implicit httpClient: HttpClient): T1 = { val res = execute() ext1.value(res) } def returning[T1, T2](ext1: Extractor[T1], ext1: Extractor[T2])(implicit httpClient: HttpClient): (T1, T2) = { val res = execute() (ext1.value(res), ext2.value(res)) } def asserting(assertions: Assertion*)(implicit client: HttpClient): Response = { val response = execute() val assertionFailures = for { assertion <- assertions failureMessage <- assertion(response) } yield failureMessage if (assertionFailures.nonEmpty) { throw assertionFailed(assertionFailures) } response } } implicit class RichExtractor[A](ext: ExtractorLike[A]) { def ===[B >: A](expected: B): Assertion = { res => val maybeValue = ext.value(res) maybeValue match { case Success(value) if (value == expected) => None case Success(value) => Some(s"${ext.name}: $value != $expected") case Failure(e) => Some(e.getMessage) } } def !==[B >: A](expected: B): Assertion = ??? def < [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? def <=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? def > [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? def >=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = ??? }}

Boile

rpla

te T

est

Dsl

Tes

t

DSL

Impl

emen

tatio

n

Page 122: REST Test - Exploring DSL design in Scala

http://en.wikipedia.org/wiki/Optical_communication

Page 123: REST Test - Exploring DSL design in Scala

Code is Communicationusing(_ url "http://api.rest.org") { implicit rb =>

GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList) val id = POST / "person" body personJson asserting (StatusCode === Status.Created) returning (header("X-Person-Id")) GET / "person" / id asserting (StatusCode === Status.OK, BodyAsPersonList === Jason) GET asserting (StatusCode === Status.OK, BodyAsPersonList === Seq(Jason)) DELETE / "person" / id asserting (StatusCode === Status.OK) GET / "person" asserting (StatusCode === Status.OK, BodyAsPersonList === EmptyList)}

Page 124: REST Test - Exploring DSL design in Scala

DSL Resources - Books

DSLs in ActionBy Debasish Ghosh

ISBN: 9781935182450http://www.manning.com/ghosh/

Scala in DepthBy Joshua D. Suereth

ISBN: 9781935182702http://www.manning.com/suereth/

Page 125: REST Test - Exploring DSL design in Scala

DSL Resources – Projects

Page 126: REST Test - Exploring DSL design in Scala
Page 127: REST Test - Exploring DSL design in Scala

http://www.flickr.com/photos/bilal-kamoon/6835060992/sizes/o/

Page 128: REST Test - Exploring DSL design in Scala

Thank You!!!

Iain Hull

Email: [email protected]: @IainHullWeb: http://IainHull.github.io

Workday