bhnat
bhnat

Reputation: 107

Negating a custom matcher in specs2

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

Answers (2)

Carlos Verdes
Carlos Verdes

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

Eric
Eric

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

Related Questions