etorreborre/specs2

Implicit conversion in ArgThat causes incorrect matching

Closed this issue · 3 comments

Using specs2-core and specs2-mock 4.20.3 the following passes:

import org.specs2.mock.mockito.ArgThat
import org.specs2.mutable.Specification

class ArgThatBugSpec extends Specification with ArgThat {

  val items: Seq[String] = Seq()

  "Things" should {
    "Fail 1" in {
      items must have size greaterThan(1000)
      items must have size lessThan(-1000)
    }
    "Fail 2" in {
      items must haveSize(greaterThan(1000))
      items must haveSize(lessThan(-1000))
    }
  }
}

Remove ArgThat and the tests fail as expected.
Example project here https://github.com/nespera/argThat-bug

Hi @nespera. That's an interesting corner case with implicit conversions. haveSize accepts both an Int and a ValueCheck[T]. Then there are 2 possible implicit conversions to go from Matcher[T] to Int (in ArgThat) and Matcher[T] to ValueCheck[T] (in org.specs2.matcher.ValueCheck).

Unfortunately it seems that one of the two conversions has to give. I couldn't find a way to make both work at the same time.
One solution is to use the ArgThat object in a specific scope, just where needed:

{
  import ArgThat._
  val myMock = mock[MyClass]
  myMock.executeMethod(greaterThan(1))
}

items must haveSize(greaterThan(1000))

Yes, I thought this might be difficult to fix. It is definitely a corner case, but one that confused me when I found it. In practice it's hard to avoid ArgThat if you're using the classes in org.specs2.mock. For example the trait org.specs2.mock.Mockito includes it via 3 different routes...

There is a workaround

trait NoArgThat extends Mockito {
    override def argThat[T, U <: T](m: org.specs2.matcher.Matcher[U]): T =
      super.argThat(m)
}

If you mix-in that trait, it should deactivate the argThat conversion. But then you lose the implicit conversion and you have to explicitly use argThat when using a matcher on mock calls.