diff --git a/src/main/scala/org/iainhull/resttest/Dsl.scala b/src/main/scala/org/iainhull/resttest/Dsl.scala index 003af24..c1d089c 100644 --- a/src/main/scala/org/iainhull/resttest/Dsl.scala +++ b/src/main/scala/org/iainhull/resttest/Dsl.scala @@ -11,7 +11,11 @@ object Dsl extends Extractors { implicit def methodToRichRequestBuilder(method: Method)(implicit builder: RequestBuilder): RichRequestBuilder = new RichRequestBuilder(methodToRequestBuilder(method)(builder)) trait Assertion { - def verify(res: Response): Unit + def result(res: Response): Option[String] + } + + def assertionFailed(assertionResults: Seq[String]): Throwable = { + new AssertionError(assertionResults.mkString(",")) } implicit class RichRequestBuilder(builder: RequestBuilder) { @@ -29,9 +33,15 @@ object Dsl extends Extractors { proc(builder) } - def asserting(assertions: Assertion*)(implicit driver: Driver): Response = { + def assert(assertions: Assertion*)(implicit driver: Driver): Response = { val res = execute() - assertions foreach (_.verify(res)) + val assertionResults = for { + a <- assertions + r <- a.result(res) + } yield r + if(assertionResults.nonEmpty) { + throw assertionFailed(assertionResults) + } res } @@ -41,56 +51,74 @@ object Dsl extends Extractors { } } - object ~ { - def unapply(res: Response): Option[(Response, Response)] = { - Some((res, res)) - } - } - def using(config: RequestBuilder => RequestBuilder)(process: RequestBuilder => Unit)(implicit builder: RequestBuilder): Unit = { process(config(builder)) } implicit class RichResponse(response: Response) { - def returning[T1](ext1: Extractor[T1])(implicit driver: Driver): T1 = { - ext1.op(response) + def returning[T1](ext1: ExtractorLike[T1])(implicit driver: Driver): T1 = { + ext1.value(response) } - def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2]): (T1, T2) = { - (ext1.op(response), ext2.op(response)) + def returning[T1, T2](ext1: ExtractorLike[T1], ext2: ExtractorLike[T2]): (T1, T2) = { + (ext1.value(response), ext2.value(response)) } - def returning[T1, T2, T3](ext1: Extractor[T1], ext2: Extractor[T2], ext3: Extractor[T3]): (T1, T2, T3) = { - (ext1.op(response), ext2.op(response), ext3.op(response)) + def returning[T1, T2, T3](ext1: ExtractorLike[T1], ext2: ExtractorLike[T2], ext3: ExtractorLike[T3]): (T1, T2, T3) = { + (ext1.value(response), ext2.value(response), ext3.value(response)) } - def returning[T1, T2, T3, T4](ext1: Extractor[T1], ext2: Extractor[T2], ext3: Extractor[T3], ext4: Extractor[T4]): (T1, T2, T3, T4) = { - (ext1.op(response), ext2.op(response), ext3.op(response), ext4.op(response)) + def returning[T1, T2, T3, T4](ext1: ExtractorLike[T1], ext2: ExtractorLike[T2], ext3: ExtractorLike[T3], ext4: ExtractorLike[T4]): (T1, T2, T3, T4) = { + (ext1.value(response), ext2.value(response), ext3.value(response), ext4.value(response)) } } implicit def requestBuilderToRichResponse(builder: RequestBuilder)(implicit driver: Driver): RichResponse = new RichResponse(builder.execute()) implicit def methodToRichResponse(method: Method)(implicit builder: RequestBuilder, driver: Driver): RichResponse = new RichResponse(builder.withMethod(method).execute()) - implicit class RichExtractor[T](ext: Extractor[T]) { - def is(expected: T): Assertion = new Assertion { - override def verify(res: Response): Unit = { - val actual = ext.op(res) - if (actual != expected) throw new AssertionError(actual + " != " + expected) + /** + * Add operator support to `Extractor`s these are used to generate an `Assertion` using the extracted value. + * + * {{{ + * GET url "http://api.rest.org/person" assert (StatusCode === Status.Ok) + * }}} + * + * == Operations == + * + * The following operations are added to all `Extractors` + * + * $ - `extractor === expected` - the extracted value is equal to the `expected` value. + * $ - `extractor !== expected` - the extracted value is not equal to the `expected` value. + * $ - `extractor in (expected1, expected2, ...)` - the extracted value is in the list of expected values. + * $ - `extractor notIn (expected1, expected2, ...)` - the extracted value is in the list of expected values. + * + * The following operations are added to `Extractor`s that support `scala.math.Ordering`. + * More precisely these operations are added to `Extractor[T]` if there exists an implicit + * `Ordering[T]` for any type `T`. + * + * $ - `extractor < expected` - the extracted value is less than the `expected` value. + * $ - `extractor <= expected` - the extracted value is less than or equal to the `expected` value. + * $ - `extractor > expected` - the extracted value is greater than the `expected` value. + * $ - `extractor <= expected` - the extracted value is greater than or equal to the `expected` value. + */ + implicit class RichExtractor[A](ext: ExtractorLike[A]) { + def ===[B >: A](expected: B): Assertion = makeAssertion(_ == expected, expected, "did not equal") + def !==[B >: A](expected: B): Assertion = makeAssertion(_ != expected, expected, "did equal") + + def < [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.lt (_, expected), expected, "was not less than") + def <=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.lteq(_, expected), expected, "was not less than or equal") + def > [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.gt( _, expected), expected, "was not greater than") + def >=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.gteq(_, expected), expected, "was not greater than or equal") + + def in[B >: A](expectedVals: B*): Assertion = makeAssertion(expectedVals.contains(_), expectedVals.mkString("(", ", ", ")"), "was not in") + def notIn[B >: A](expectedVals: B*): Assertion = makeAssertion(!expectedVals.contains(_), expectedVals.mkString("(", ", ", ")"), "was in") + + private def makeAssertion[B](pred: A => Boolean, expected: Any, text: String) = new Assertion { + override def result(res: Response): Option[String] = { + val actual = ext.value(res) + if (!pred(actual)) Some(s"${ext.name}: $actual $text $expected") else None } } - def isNot(expected: T): Assertion = new Assertion { - override def verify(res: Response): Unit = { - val actual = ext.op(res) - if (actual == expected) throw new AssertionError(actual + " == " + expected) - } - } - def isIn(expectedVals: T*): Assertion = new Assertion { - override def verify(res: Response): Unit = { - val actual = ext.op(res) - if (!expectedVals.contains(actual)) throw new AssertionError(actual + " not in " + expectedVals) - } - } } } \ No newline at end of file diff --git a/src/main/scala/org/iainhull/resttest/Extractors.scala b/src/main/scala/org/iainhull/resttest/Extractors.scala index 781680a..eada72f 100644 --- a/src/main/scala/org/iainhull/resttest/Extractors.scala +++ b/src/main/scala/org/iainhull/resttest/Extractors.scala @@ -3,9 +3,94 @@ package org.iainhull.resttest import Api._ import scala.util.Try -case class Extractor[+T](name: String, op: Response => T) { - def unapply(res: Response): Option[T] = Try { op(res) }.toOption - def value(implicit res: Response): T = op(res) +trait ExtractorLike[+A] { + def name: String + def unapply(res: Response): Option[A] = Try { value(res) }.toOption + def value(implicit res: Response): A +} + +/** + * Primary implementation of ExtractorLike. The name and extraction + * + * @param name + * The name of the extractor + * @param op + * The operation to extract the value of type `A` from the `Response` + */ +case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { + def value(implicit res: Response): A = op(res) + + /** + * Create a new `Extractor` by executing a new function to modify the result. + * Normally followed by `as`. + * + * {{{ + * val JsonBody = BodyText andThen Json.parse as "JsonBody" + * }}} + */ + def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) + + /** + * Rename the extractor + */ + def as(newName: String) = copy(name = newName) +} + +case class Header(header: String) extends ExtractorLike[String] { + val asText = Extractor[String]("Header(" + header + ")", _.headers(header).mkString(",")) + val asList = Extractor[List[String]]("Header(" + header + ").asList", _.headers(header)) + val asOption = Extractor[Option[List[String]]]("Header(" + header + ").asOption", _.headers.get(header)) + val isDefined = new Object { + def unapply(res: Response): Boolean = { + res.headers.contains(header) + } + } + + def ->(value: String): (String, String) = { + (header, value) + } + + override def name: String = asText.name + override def unapply(res: Response): Option[String] = asText.unapply(res) + override def value(implicit res: Response): String = asText.value + +} + +object Header { + val AccessControlAllowOrigin = Header("Access-Control-Allow-Origin") + val AcceptRanges = Header("Accept-Ranges") + val Age = Header("Age") + val Allow = Header("Allow") + val CacheControl = Header("Cache-Control") + val Connection = Header("Connection") + val ContentEncoding = Header("Content-Encoding") + val ContentLanguage = Header("Content-Language") + val ContentLength = Header("Content-Length") + val ContentLocation = Header("Content-Location") + val ContentMd5 = Header("Content-MD5") + val ContentDisposition = Header("Content-Disposition") + val ContentRange = Header("Content-Range") + val ContentType = Header("Content-Type") + val Date = Header("Date") + val ETag = Header("ETag") + val Expires = Header("Expires") + val LastModified = Header("Last-Modified") + val Link = Header("Link") + val Location = Header("Location") + val P3P = Header("P3P") + val Pragma = Header("Pragma") + val ProxyAuthenticate = Header("Proxy-Authenticate") + val Refresh = Header("Refresh") + val RetryAfter = Header("Retry-After") + val Server = Header("Server") + val SetCookie = Header("Set-Cookie") + val StrictTransportSecurity = Header("Strict-Transport-Security") + val Trailer = Header("Trailer") + val TransferEncoding = Header("Transfer-Encoding") + val Vary = Header("Vary") + val Via = Header("Via") + val Warning = Header("Warning") + val WwwAuthenticate = Header("WWW-Authenticate") } trait Extractors { @@ -18,61 +103,23 @@ trait Extractors { val BodyText = Extractor[String]("body", _.body.get) - def headerText(name: String) = Extractor[String]("header(" + name + ")", _.headers(name).mkString(",")) - - def headerList(name: String) = Extractor[List[String]]("headerList(" + name + ")", _.headers(name)) - - def header(name: String) = Extractor[Option[List[String]]]("headerOption(" + name + ")", _.headers.get(name)) - - class Header(name: String) { - lazy val list = headerList(name) - lazy val text = headerText(name) - lazy val present = new Object { - def unapply(res: Response): Boolean = { - res.headers.contains(name) - } - } - def -> (value: String): (String, String) = { - (name, value) + /** + * Enable Extractors to be chained together in case clauses. + * + * For example: + * {{{ + * GET / id expecting { + * case StatusCode(Status.OK) ~ Header.ContentType(ct) ~ BodyAsPerson(person) => + * ct should be("application/json") + * person should be(Jason) + * } + * }}} + */ + object ~ { + def unapply(res: Response): Option[(Response, Response)] = { + Some((res, res)) } } - - object Header { - val AccessControlAllowOrigin = new Header("Access-Control-Allow-Origin") - val AcceptRanges = new Header("Accept-Ranges") - val Age = new Header("Age") - val Allow = new Header("Allow") - val CacheControl = new Header("Cache-Control") - val Connection = new Header("Connection") - val ContentEncoding = new Header("Content-Encoding") - val ContentLanguage = new Header("Content-Language") - val ContentLength = new Header("Content-Length") - val ContentLocation = new Header("Content-Location") - val ContentMd5 = new Header("Content-MD5") - val ContentDisposition = new Header("Content-Disposition") - val ContentRange = new Header("Content-Range") - val ContentType = new Header("Content-Type") - val Date = new Header("Date") - val ETag = new Header("ETag") - val Expires = new Header("Expires") - val LastModified = new Header("Last-Modified") - val Link = new Header("Link") - val Location = new Header("Location") - val P3P = new Header("P3P") - val Pragma = new Header("Pragma") - val ProxyAuthenticate = new Header("Proxy-Authenticate") - val Refresh = new Header("Refresh") - val RetryAfter = new Header("Retry-After") - val Server = new Header("Server") - val SetCookie = new Header("Set-Cookie") - val StrictTransportSecurity = new Header("Strict-Transport-Security") - val Trailer = new Header("Trailer") - val TransferEncoding = new Header("Transfer-Encoding") - val Vary = new Header("Vary") - val Via = new Header("Via") - val Warning = new Header("Warning") - val WwwAuthenticate = new Header("WWW-Authenticate") - } } object Extractors extends Extractors { diff --git a/src/main/scala/org/iainhull/resttest/JerseyDriver.scala b/src/main/scala/org/iainhull/resttest/JerseyDriver.scala index b70ef95..2213b5d 100644 --- a/src/main/scala/org/iainhull/resttest/JerseyDriver.scala +++ b/src/main/scala/org/iainhull/resttest/JerseyDriver.scala @@ -3,6 +3,7 @@ package org.iainhull.resttest import com.sun.jersey.api.client.Client import com.sun.jersey.api.client.ClientResponse import scala.collection.JavaConverters +import com.sun.jersey.api.client.WebResource object Jersey { import Api._ @@ -16,14 +17,35 @@ object Jersey { } def createClientResponse(request: Request): ClientResponse = { - val resource = jersey.resource(request.url) + val builder = addRequestHeaders(request.headers, jersey.resource(request.url).getRequestBuilder) + + for (b <- request.body) { + builder.entity(b) + } + request.method match { - case GET => resource.get(classOf[ClientResponse]) - case POST => resource.post(classOf[ClientResponse]) - case PUT => resource.put(classOf[ClientResponse]) - case DELETE => resource.delete(classOf[ClientResponse]) + case GET => builder.get(classOf[ClientResponse]) + case POST => builder.post(classOf[ClientResponse]) + case PUT => builder.put(classOf[ClientResponse]) + case DELETE => builder.delete(classOf[ClientResponse]) } } + + def addRequestHeaders(headers: Map[String, List[String]], builder: WebResource#Builder): WebResource#Builder = { + def forAllNames(names: List[String], b: WebResource#Builder): WebResource#Builder = { + names match { + case h :: t => forAllNames(t, forAllValues(h, headers(h), b)) + case Nil => b + } + } + def forAllValues(name: String, values: List[String], b: WebResource#Builder): WebResource#Builder = { + values match { + case h :: t => forAllValues(name, t, b.header(name, h)) + case Nil => b + } + } + forAllNames(headers.keys.toList, builder) + } def headers(response: ClientResponse): Map[String, List[String]] = { import JavaConverters._ diff --git a/src/main/scala/org/iainhull/resttest/JsonExtractors.scala b/src/main/scala/org/iainhull/resttest/JsonExtractors.scala index 80f673c..59a21ea 100644 --- a/src/main/scala/org/iainhull/resttest/JsonExtractors.scala +++ b/src/main/scala/org/iainhull/resttest/JsonExtractors.scala @@ -30,31 +30,35 @@ trait JsonExtractors { /** * Extract the response body as a json document */ - val jsonBody = Extractor[JsValue]("jsonBody", BodyText.op andThen Json.parse) + val JsonBody = BodyText andThen Json.parse as "JsonBody" /** * Extract the response body as an object. */ - def jsonBodyAs[T: Reads]: Extractor[T] = jsonBodyAs(JsPath) + def jsonBodyAs[T: Reads](implicit tag : reflect.ClassTag[T] ): Extractor[T] = jsonBodyAs(JsPath) /** * Extract a portion of the response body as an object. * * @param path the path for the portion of the response to use */ - def jsonBodyAs[T: Reads](path: JsPath = JsPath) = Extractor[T]("jsonBodyAs", jsonBody.op andThen (jsonToValue(_, path))) + def jsonBodyAs[T: Reads](path: JsPath = JsPath)(implicit tag : reflect.ClassTag[T] ) = { + JsonBody andThen (jsonToValue(_, path)) as (s"JsonBodyAs[${tag.runtimeClass.getName}]") + } /** * Extract the response body as a List of objects. */ - def jsonBodyAsList[T: Reads]: Extractor[Seq[T]] = jsonBodyAsList(JsPath) + def jsonBodyAsList[T: Reads](implicit tag : reflect.ClassTag[T] ): Extractor[Seq[T]] = jsonBodyAsList(JsPath) /** * Extract a portion of the response body as a List of objects. * * @param path the path for the portion of the response to use */ - def jsonBodyAsList[T: Reads](path: JsPath = JsPath) = Extractor[Seq[T]]("jsonBodyAsList", jsonBody.op andThen (jsonToList(_, path))) + def jsonBodyAsList[T: Reads](path: JsPath = JsPath)(implicit tag : reflect.ClassTag[T] ) = { + JsonBody andThen (jsonToList(_, path)) as (s"JsonBodyAsList[${tag.runtimeClass.getName}]") + } } object JsonExtractors extends JsonExtractors \ No newline at end of file diff --git a/src/main/scala/org/iainhull/resttest/RestMatchers.scala b/src/main/scala/org/iainhull/resttest/RestMatchers.scala index 3f11738..29a8a8b 100644 --- a/src/main/scala/org/iainhull/resttest/RestMatchers.scala +++ b/src/main/scala/org/iainhull/resttest/RestMatchers.scala @@ -4,6 +4,7 @@ import org.scalatest.matchers.ShouldMatchers.AnyRefShouldWrapper import org.scalatest.matchers.HavePropertyMatcher import org.scalatest.matchers.HavePropertyMatchResult import org.scalatest.matchers.ShouldMatchers.AnyShouldWrapper +import org.scalatest.Assertions /** * Adds [[http://www.scalatest.org/ ScalaTest]] support to the RestTest [[Dsl]]. @@ -11,7 +12,7 @@ import org.scalatest.matchers.ShouldMatchers.AnyShouldWrapper * The `should` keyword is added to [[RequestBuilder]] and [[Response]] expressions, the * `RequestBuilder` is executed first and `should` applied to the `Response`. * - * The `have` keyword supports [[Extractor]]s. See [[ExtractorToHavePropertyMatcher]] for more details. + * The `have` keyword supports [[ExtractorLike]]s. See [[ExtractorToHavePropertyMatcher]] for more details. * * == Example == * @@ -61,10 +62,11 @@ trait RestMatchers { requestBuilderToShouldWrapper(builder.withMethod(method)) } - - implicit def extractorToShouldWrapper[T](extractor: Extractor[T])(implicit response: Response): AnyShouldWrapper[T] = { - val v: T = extractor.op(response) - new AnyShouldWrapper[T](v) + implicit def extractorToShouldWrapper[T](extractor: ExtractorLike[T])(implicit response: Response): AnyShouldWrapper[T] = { + Assertions.withClue(extractor.name) { + val v: T = extractor.value + new AnyShouldWrapper[T](v) + } } /** @@ -74,78 +76,17 @@ trait RestMatchers { * * {{{ * response should have(statusCode(Status.OK)) - * response should have(headerText("header2") === "value") - * response should have(statusCode !== 1) - * response should have(statusCode > 1) - * response should have(statusCode in (Status.OK, Status.Created)) - * response should have(statusCode between (400, 499)) * }}} - * - * == Operations == - * - * The following operations are added to all `Extractors` - * - * $ - `extractor(expected)` - the extracted value is equal to the `expected` value. - * $ - `extractor === expected` - the extracted value is equal to the `expected` value. - * $ - `extractor !== expected` - the extracted value is not equal to the `expected` value. - * $ - `extractor in (expected1, expected2, ...)` - the extracted value is in the list of expected values. - * - * The following operations are added to `Extractor`s that support `scala.math.Ordering`. - * More precisely these operations are added to `Extractor[T]` if there exists an implicit - * `Ordering[T]` for any type `T`. - * - * $ - `extractor < expected` - the extracted value is less than the `expected` value. - * $ - `extractor <= expected` - the extracted value is less than or equal to the `expected` value. - * $ - `extractor > expected` - the extracted value is greater than the `expected` value. - * $ - `extractor <= expected` - the extracted value is greater than or equal to the `expected` value. - * $ - `extractor between (lowExpected, highExpected)` - the extracted value is greater than or equal to `lowExpected` and less than or equal to `highExpected`. */ - implicit class ExtractorToHavePropertyMatcher[T](extractor: Extractor[T]) { - def apply(expected: T) = makeMatcher(_ == _, expected) - - def ===(expected: T) = makeMatcher(_ == _, expected) - def !==(expected: T) = makeMatcher(_ != _, expected, "!= ") - - def <(expected: T)(implicit ord: math.Ordering[T]) = makeMatcher(ord.lt, expected, "< ") - def <=(expected: T)(implicit ord: math.Ordering[T]) = makeMatcher(ord.lteq, expected, "<= ") - def >(expected: T)(implicit ord: math.Ordering[T]) = makeMatcher(ord.gt, expected, "> ") - def >=(expected: T)(implicit ord: math.Ordering[T]) = makeMatcher(ord.gteq, expected, ">= ") - - private def makeMatcher(pred: (T, T) => Boolean, expected: T, expectedHint: String = ""): HavePropertyMatcher[Response, String] = { - new HavePropertyMatcher[Response, String] { - def apply(response: Response) = { - val actual = extractor.op(response) - new HavePropertyMatchResult( - pred(actual, expected), - extractor.name, - expectedHint + expected.toString, - actual.toString) - } - } - } - - def in(firstExpected: T, moreExpected: T*): HavePropertyMatcher[Response, String] = { - val allExpected = firstExpected +: moreExpected - new HavePropertyMatcher[Response, String] { - def apply(response: Response) = { - val actual = extractor.op(response) - new HavePropertyMatchResult( - allExpected.contains(actual), - extractor.name, - "in " + allExpected.mkString("(", ",", ")"), - actual.toString) - } - } - } - - def between(lowExpected: T, highExpected: T)(implicit ord: math.Ordering[T]): HavePropertyMatcher[Response, String] = { + implicit class ExtractorToHavePropertyMatcher[T](extractor: ExtractorLike[T]) { + def apply(expected: T): HavePropertyMatcher[Response, String] = { new HavePropertyMatcher[Response, String] { def apply(response: Response) = { - val actual = extractor.op(response) + val actual = extractor.value(response) new HavePropertyMatchResult( - ord.lteq(lowExpected, actual) && ord.lteq(actual, highExpected), + actual == expected, extractor.name, - "between (" + lowExpected + "," + highExpected + ")", + expected.toString, actual.toString) } } @@ -162,9 +103,9 @@ trait RestMatchers { * response should have(body) * }}} */ - implicit class OptionExtractorToHavePropertyMatcher(extractor: Extractor[Option[_]]) extends HavePropertyMatcher[Response, String] { + implicit class OptionExtractorToHavePropertyMatcher(extractor: ExtractorLike[Option[_]]) extends HavePropertyMatcher[Response, String] { def apply(response: Response) = { - val actual = extractor.op(response) + val actual = extractor.value(response) new HavePropertyMatchResult( actual.isDefined, extractor.name, diff --git a/src/test/scala/org/iainhull/resttest/DslSpec.scala b/src/test/scala/org/iainhull/resttest/DslSpec.scala index d30e77f..51ae1bf 100644 --- a/src/test/scala/org/iainhull/resttest/DslSpec.scala +++ b/src/test/scala/org/iainhull/resttest/DslSpec.scala @@ -102,9 +102,16 @@ class DslSpec extends FlatSpec with ShouldMatchers { driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person?page=2&per_page=100"))) } } - + + it should "support abstracting common values with using method" in { + using(_ url "http://api.rest.org/") { implicit rb => + GET / 'person :? ('page -> 2, 'per_page -> 100) execute () + driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person?page=2&per_page=100"))) + } + } + it should "support returning values from the response" in { - RequestBuilder() withUrl "http://api.rest.org/person/" apply { implicit rb => + using(_ url "http://api.rest.org/person/") { implicit rb => val (c1, b1) = GET returning (StatusCode, BodyText) driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person/"))) c1 should be(Status.OK) @@ -112,14 +119,51 @@ class DslSpec extends FlatSpec with ShouldMatchers { } } - it should "support asserting values from the response" in { - RequestBuilder() withUrl "http://api.rest.org/person/" apply { implicit rb => - GET asserting (StatusCode is Status.OK) + it should "support asserting values equals check" in { + using(_ url "http://api.rest.org/person/") { implicit rb => + GET assert (StatusCode === Status.OK) driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person/"))) + + GET assert (Header("X-Person-Id") === "1234") - val e = evaluating { GET asserting (StatusCode is Status.Created) } should produce[AssertionError] + val e = evaluating { GET assert (StatusCode === Status.Created) } should produce[AssertionError] + driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person/"))) + e should have('message("statusCode: 200 did not equal 201")) + } + } + + it should "support asserting values not-equals check" in { + using(_ url "http://api.rest.org/person/") { implicit rb => + GET assert (StatusCode !== Status.Created) + driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person/"))) + + GET assert (Header("X-Person-Id") !== "999") + + val e = evaluating { GET assert (StatusCode !== Status.OK) } should produce[AssertionError] + e should have('message("statusCode: 200 did equal 200")) + } + } + + it should "support asserting values in check" in { + using(_ url "http://api.rest.org/person/") { implicit rb => + GET assert (StatusCode in (Status.OK, Status.Created)) + driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person/"))) + + val e = evaluating { GET assert (StatusCode in (Status.Created, Status.Accepted)) } should produce[AssertionError] + e should have('message("statusCode: 200 was not in (201, 202)")) + } + } + + it should "support asserting values Ordered comparison operator checks" in { + using(_ url "http://api.rest.org/person/") { implicit rb => + GET assert (StatusCode > 1) + GET assert (StatusCode >= 1) + GET assert (StatusCode < 299) + GET assert (StatusCode <= 299) + + val e = evaluating { GET assert (StatusCode > 999) } should produce[AssertionError] driver.lastRequest should have('method(GET), 'url(new URI("http://api.rest.org/person/"))) - e should have('message("200 != 201")) + e should have('message("statusCode: 200 was not greater than 999")) } } @@ -212,7 +256,7 @@ class DslSpec extends FlatSpec with ShouldMatchers { it should "support returning values from the response" in { RequestBuilder() url "http://api.rest.org/person/" apply { implicit rb => val (c1, b1) = GET returning (StatusCode, Body) - val (c2, id) = POST body personJson returning (StatusCode, headerText("X-Person-Id")) + val (c2, id) = POST body personJson returning (StatusCode, Header("X-Person-Id")) val (c3, b3) = GET / id returning (StatusCode, Body) val (c4, b4) = GET returning (StatusCode, Body) val c5 = DELETE / id returning StatusCode @@ -234,13 +278,13 @@ class DslSpec extends FlatSpec with ShouldMatchers { Nil RequestBuilder() url "http://api.rest.org/person" apply { implicit rb => - GET asserting (StatusCode is Status.OK, jsonBodyAsList[Person] is EmptyList) - val id = POST body personJson asserting (StatusCode is Status.Created) returning (headerText("X-Person-Id")) - GET / id asserting (StatusCode is Status.OK, jsonBodyAs[Person] is Jason) - GET asserting (StatusCode is Status.OK, jsonBodyAsList[Person] is Seq(Jason)) - DELETE / id asserting (StatusCode is Status.OK) - GET / id asserting (StatusCode is Status.NotFound) - GET asserting (StatusCode is Status.OK, jsonBodyAsList[Person] is EmptyList) + GET assert (StatusCode === Status.OK, jsonBodyAsList[Person] === EmptyList) + val id = POST body personJson assert (StatusCode === Status.Created) returning (Header("X-Person-Id")) + GET / id assert (StatusCode === Status.OK, jsonBodyAs[Person] === Jason) + GET assert (StatusCode === Status.OK, jsonBodyAsList[Person] === Seq(Jason)) + DELETE / id assert (StatusCode === Status.OK) + GET / id assert (StatusCode === Status.NotFound) + GET assert (StatusCode === Status.OK, jsonBodyAsList[Person] === EmptyList) } } @@ -259,7 +303,7 @@ class DslSpec extends FlatSpec with ShouldMatchers { val BodyAsPersonList = jsonBodyAsList[Person] val BodyAsPerson= jsonBodyAs[Person] - val PersonIdHeader = new Header("X-Person-Id").text + val PersonIdHeader = Header("X-Person-Id") using(_ url "http://api.rest.org/person") { implicit rb => GET expecting { diff --git a/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala b/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala index 8c8ea49..9abe5e6 100644 --- a/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala +++ b/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala @@ -12,7 +12,7 @@ class ExtractorsSpec extends FlatSpec with ShouldMatchers { val response = Response(Status.OK, toHeaders("SimpleHeader" -> "SimpleValue", "MultiHeader" -> "Value1", "MultiHeader" -> "Value2"), Some("body")) - def returning[T](ext: Extractor[T]): T = ext.op(response) + def returning[T](ext: ExtractorLike[T]): T = ext.value(response) "statusCode" should "return the responses statusCode" in { returning(StatusCode) should be(Status.OK) @@ -30,24 +30,25 @@ class ExtractorsSpec extends FlatSpec with ShouldMatchers { returning(Body) should be(Option("body")) } - "header" should "return the responses header value as an Option[List[String]]" in { - returning(header("SimpleHeader")) should be(Some(List("SimpleValue"))) - returning(header("MultiHeader")) should be(Some(List("Value1","Value2"))) + "Header" should "return the responses header value as a String" in { + returning(Header("SimpleHeader")) should be("SimpleValue") + returning(Header("MultiHeader")) should be("Value1,Value2") - returning(header("NotAHeader")) should be(None) + evaluating { returning(Header("NotAHeader")) } should produce [NoSuchElementException] } - "headerText" should "return the responses header value as a String" in { - returning(headerText("SimpleHeader")) should be("SimpleValue") - returning(headerText("MultiHeader")) should be("Value1,Value2") + "Header.asOption" should "return the responses header value as an Option[List[String]]" in { + returning(Header("SimpleHeader").asOption) should be(Some(List("SimpleValue"))) + returning(Header("MultiHeader").asOption) should be(Some(List("Value1","Value2"))) - evaluating { returning(headerText("NotAHeader")) } should produce [NoSuchElementException] + returning(Header("NotAHeader").asOption) should be(None) } - "headerList" should "return the responses header as a list" in { - returning(headerList("SimpleHeader")) should be(List("SimpleValue")) - returning(headerList("MultiHeader")) should be(List("Value1","Value2")) + + "Header.asList" should "return the responses header as a list" in { + returning(Header("SimpleHeader").asList) should be(List("SimpleValue")) + returning(Header("MultiHeader").asList) should be(List("Value1","Value2")) - evaluating { returning(headerList("NotAHeader")) } should produce [NoSuchElementException] + evaluating { returning(Header("NotAHeader").asList) } should produce [NoSuchElementException] } } \ No newline at end of file diff --git a/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala b/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala index 101ac5c..d00cb72 100644 --- a/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala +++ b/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala @@ -23,40 +23,20 @@ class RestMatcherSpec extends FlatSpec with ShouldMatchers { "RestMatchers" should "support 'have property' equals check" in { response should have('statusCode(Status.OK)) response should have(StatusCode(Status.OK)) - response should have(StatusCode === Status.OK) - response should have(headerText("header2") === "value") - } - - it should "support 'have property' not-equals check" in { - response should have(StatusCode !== 1) - response should have(headerText("header2") !== "not value") - response should have(headerList("header2") !== List("not value")) - } - - it should "support 'have property' in check" in { - response should have(StatusCode in (Status.OK, Status.Created)) + response should have(Header("header2")("value")) } it should "support 'have property' check for Extractor[Option[_]]" in { - response should have(header("header1")) + response should have(Header("header1").asOption) response should not(have(Body)) } - it should "support 'have property' Ordered comparison operator checks" in { - response should have(StatusCode > 1) - response should have(StatusCode >= 1) - response should have(StatusCode < 299) - response should have(StatusCode <= 299) - } - - it should "support 'have property' Ordered between operator checks" in { - response should have(StatusCode between (200, 299)) - } - "Sample use-case" should "support asserting on values from the response with have matchers" in { import JsonExtractors._ val EmptyList = Seq() + val BodyAsListPerson = jsonBodyAsList[Person] + val BodyAsPerson = jsonBodyAs[Person] driver.responses = Response(Status.OK, Map(), Some("[]")) :: Response(Status.Created, toHeaders("X-Person-Id" -> "99"), None) :: @@ -68,20 +48,20 @@ class RestMatcherSpec extends FlatSpec with ShouldMatchers { Nil using(_ url "http://api.rest.org/person") { implicit rb => - GET should have(StatusCode(Status.OK), jsonBodyAsList[Person] === EmptyList) + GET should have(StatusCode(Status.OK), BodyAsListPerson(EmptyList)) - val (status, id) = POST body personJson returning (StatusCode, headerText("X-Person-Id")) + val (status, id) = POST body personJson returning (StatusCode, Header("X-Person-Id")) status should be(Status.Created) - val foo = GET / id should have(StatusCode(Status.OK), jsonBodyAs[Person] === Jason) + val foo = GET / id should have(StatusCode(Status.OK), BodyAsPerson(Jason)) - GET should have(StatusCode === Status.OK, jsonBodyAsList[Person] === Seq(Jason)) + GET should have(StatusCode(Status.OK), BodyAsListPerson(Seq(Jason))) - DELETE / id should have(StatusCode === Status.OK) + DELETE / id should have(StatusCode(Status.OK)) - GET / id should have(StatusCode === Status.NotFound) + GET / id should have(StatusCode(Status.NotFound)) - GET should have(StatusCode(Status.OK), jsonBodyAsList[Person] === EmptyList) + GET should have(StatusCode(Status.OK), BodyAsListPerson( EmptyList)) } } @@ -106,7 +86,7 @@ class RestMatcherSpec extends FlatSpec with ShouldMatchers { val id = POST body personJson expecting { implicit res => StatusCode should be(Status.Created) - headerText("X-Person-Id").value + Header("X-Person-Id").value } GET / id expecting { implicit res => diff --git a/src/test/scala/org/iainhull/resttest/UnapplySpec.scala b/src/test/scala/org/iainhull/resttest/UnapplySpec.scala index 63f501d..93a0dfc 100644 --- a/src/test/scala/org/iainhull/resttest/UnapplySpec.scala +++ b/src/test/scala/org/iainhull/resttest/UnapplySpec.scala @@ -8,7 +8,6 @@ import org.scalatest.matchers.ShouldMatchers import Api._ object ~ { - def unapply(res: Response): Option[(Response, Response)] = { Some((res, res)) } @@ -28,7 +27,7 @@ class UnapplySpec extends FlatSpec with ShouldMatchers { val res = Response(Status.OK, toHeaders("X-Person-Id" -> "1234", Header.ContentType -> "application/json"), Some(personJson)) - val HeaderId = headerText("X-Person-Id") + val HeaderId = Header("X-Person-Id") val BodyAsPerson = jsonBodyAs[Person] // GET / "foo" expecting { @@ -37,10 +36,10 @@ class UnapplySpec extends FlatSpec with ShouldMatchers { expecting(res) { - case StatusCode(Status.OK) ~ HeaderId(h) ~ Header.ContentType.list(l) ~ BodyAsPerson(p) => + case StatusCode(Status.OK) ~ HeaderId(h) ~ Header.ContentType.asList(l) ~ BodyAsPerson(p) => l should be(List("application/json")) h should be ("1234") p.name should be ("Jason") } } -} \ No newline at end of file +}