Reputation: 107
I had written a custom matcher in specs2 as follows:
object MyMatchers {
def haveHttpStatus(expected:Int) = new StatusMatcher(expected)
}
class StatusMatcher(expected:Int) extends Matcher[Option[Future[Result]]] {
def apply[R <: Option[Future[Result]]](r: Expectable[R]) = {
val v = r.value
v match {
case None => failure(s"${r.description} was None", r)
case Some(fr:Future[Result]) =>
import play.api.test.Helpers._
val actual:Int = status(fr)
result(actual == expected,
s"${r.description} has status $actual as expected",
s"${r.description} expected status $expected but found $actual",
r)
case _ =>
failure(s"${r.description} has unexpected type $v", r)
}
}
}
When I test for the positive case, it works as expected:
"return OK" in new WithApplication {
val response = route(FakeRequest(HttpVerbs.GET, "/test"))
import tools.MyMatchers._
response must haveHttpStatus(OK)
}
But when I try to test a negative case, I get a compile error, "value haveHttpStatus is not a member of org.specs2.matcher.MatchResult[Option[scala.concurrent.Future[play.api.mvc.Result]]]"
"return OK" in new WithApplication {
val response = route(FakeRequest(HttpVerbs.GET, "/test"))
import tools.MyMatchers._
response must not haveHttpStatus(OK)
}
I saw in one example (https://gist.github.com/seratch/1414177) where the custom matcher was wrapped in parentheses. This worked. Putting the 'not' at the end also worked.
"return OK" in new WithApplication {
val response = route(FakeRequest(HttpVerbs.GET, "/test"))
import tools.MyMatchers._
response must not (haveHttpStatus(OK))
}
"also return OK" in new WithApplication {
val response = route(FakeRequest(HttpVerbs.GET, "/test"))
import tools.MyMatchers._
response must haveHttpStatus(OK) not
}
But I'm not really clear on why these two approaches work, but the original attempt at negation doesn't. If anyone can shed some light on this, I'd really like to understand the differences in each approach. This is in a Play Framework 2.4.6 project, including specs2 as specs2 % Test
.
To look at the types returned, and I found:
"return OK" in new WithApplication {
val response = route(FakeRequest(HttpVerbs.GET, "/test"))
import tools.MyMatchers._
val matcher1 = haveHttpStatus(OK) // <-- is type StatusMatcher
val matcher2 = (haveHttpStatus(OK)) // <-- is type StatusMatcher
val matcher3 = not (haveHttpStatus(OK)) // <-- is type AnyRef with Matcher[Option[Future[Result]]]
val matcher4 = not haveHttpStatus(OK) // <-- doesn't compile - gives the error "value haveHttpStatus is not a member of org.specs2.matcher.NotMatcher[Any]"
response must haveHttpStatus(OK)
}
Looking through AnyBeHaveMatchers, it looks like I need haveHttpStatus to return a MatchResult, rather than a StatusMatcher, but I'm having a hard time getting from here to there.
Update:
I drilled through SizedCheckedMatcher, which is then used in the TraversableBaseMatchers trait as
def haveSize[T : Sized](check: ValueCheck[Int]) = new SizedCheckedMatcher[T](check, "size")
Then in TraversableBeHaveMatchers, there is the class HasSize, which returns a MatchResult when you call
def size(n: Int) : MatchResult[T] = s(outer.haveSize[T](n))
This is pretty much then same as the CustomMatcher example in https://github.com/etorreborre/specs2/blob/master/tests/src/test/scala/org/specs2/matcher/LogicalMatcherSpec.scala.
The issue I'm hitting trying to replicate either is that when calling s() or result(), I get the compile error
method apply in trait MatchResult cannot be accessed in org.specs2.matcher.MatchResult[Option[scala.concurrent.Future[play.api.mvc.Result]]]
Upvotes: 0
Views: 885
Reputation: 3147
The reason why your example is not working is because you use infix notation.
The easiest solution is to put the custom matcher between ():
response must not (haveHttpStatus(OK))
Or putting at the end:
response must haveHttpStatus(OK) not
Why original code doesn't work? Check how the compiler interprets next expression:
// original code that doesn't work
not haveHttpStatus(OK)
// how compiler translates it
not.haveHttpStatus(OK)
As you can see the compiler tries to find a the method haveHttpStatus
as part of NotMatcher[Any]
which is not implemented (unless as another answer mentioned you implement it as an extension method).
However if you add ()
the compiler understand the full code inside them is the body of the infixed method, and not an extension method:
// original code that works
not (haveHttpStatus(OK))
// compiler reads
not(haveHttpStatus(OK))
Upvotes: 0
Reputation: 15557
If you want to use a syntax with a must not beOk[A]
where beOk
is a custom matcher you need to provide an implicit conversion (an example here):
implicit class NotStatusMatcherMatcher(result: NotMatcher[Option[Future[Result]]]) {
def haveHttpStatus(expected:Int) =
result.applyMatcher(MyMatchers.haveHttpStatus(expected))
}
And by the way a simpler way to create custom matchers is to use implicit conversions:
import org.specs2.matcher.MatcherImplicits._
type Res = Option[Future[Result]]
def haveHttpStatus(expectedStatus: Int): Matcher[Res] = { actual: Res =>
actual match {
case None =>
(false, s"the result was None")
case Some(fr:Future[Result]) =>
import play.api.test.Helpers._
val actualStatus = status(fr)
(actualStatus == expectedStatus,
s"expected status $expectedStatus but found $actualStatus")
case v =>
(false, s"unexpected type ${v.getClass}")
}
}
Upvotes: 0