mockito-scala icon indicating copy to clipboard operation
mockito-scala copied to clipboard

Matching an AnyVal with private constructor

Open Krever opened this issue 2 years ago • 8 comments

I'm not sure if it's limited to AnyVals or applies to any class with a private constructor, but it seems to be impossible to match arguments of such type.

Pseudo-example:

final case class CountryCode private (value: String) extends AnyVal
object CountryCode extends LazyLogging {
  def apply(code: String): Either[String, CountryCode] = { ... }
  }
}

foo.bar(any[CountryCode]).returns(baz)

fails compilations with

constructor CountryCode in class CountryCode cannot be accessed in <$anon: org.mockito.matchers.DefaultValueProvider[CountryCode]> from <$anon: org.mockito.matchers.DefaultValueProvider[CountryCode]>
        .bar(any[CountryCode])

I haven't found a workaround for now

Krever avatar Jul 26 '21 12:07 Krever

Scala version? (iirc on each version I use a different ~hack~ impl 😂 )

ultrasecreth avatar Jul 26 '21 13:07 ultrasecreth

2.13.4

Krever avatar Jul 26 '21 14:07 Krever

Well, here your code doesn't make it any easy, as there's no way for mockito to know how to construct a "default" value for it (value classes need a default value cause a reference to a value class can't ever be null)

Luckily the design of mockito-scala allows you to circumbent this (yay to past me!!) XD

So there's this type class DefaultValueProvider that's failing for you, the default impl is a macro that'll try to construct a "base" or "empty" instance of your type, which in your case is not only not possible given the private constructor, but also cause the apply method returns a different type Either (is not something the macro attempts at the moment, but even if it did so, it wouldn't work here).

In any case, the solution is pretty simple, provide an implicit that knows how to build a default value and that's it, something along these lines.

final case class CountryCode private (value: String) extends AnyVal
object CountryCode extends LazyLogging {
  def apply(code: String): Either[String, CountryCode] = { ... }
  }
}

implicit val defaultValueProvider: DefaultValueProvider[CountryCode] =
      new DefaultValueProvider[CountryCode] {
        override def default: CountryCode = CountryCode("").value
      }

foo.bar(any[CountryCode]).returns(baz)

Let me know how it goes :)

ultrasecreth avatar Jul 26 '21 19:07 ultrasecreth

Oh wow, that's pretty neat and it worked! One unfortunate consequence is that in practice my bar had multiple arguments and because of how type inference works, I had to explicitly provide type params to all wildcards (bar(*[CountryCode], *[Thing1], *[Thing2] and so on). Luckily for me, I had this exact problem in one helper function, but if it was distributed more widely, it would be quite a problem.

One "solution" I see is to use a different type, like DefaultValueProviderException (as in exception from the rule), that would be detected by the default impl and used for types for which it is defined. But this would be rather complicated, so maybe there is a simpler way?

On top of that, do you have control over original error message so it could point to the solution?

Krever avatar Jul 27 '21 05:07 Krever

Were all the params value classes? afaik, you only need to provide the types for value classes.

ultrasecreth avatar Jul 29 '21 09:07 ultrasecreth

Nope, only a single one but parameters become necessary because once I put DefaultValueProvider[T] in scope, type inference tries to use it everywhere where type param is not provided (infers the type param by the available implicit instance I think).

Krever avatar Jul 29 '21 09:07 Krever

Ohh, you can also make it not-implicit, it is just a param on the any method, so you can pass it manually too

ultrasecreth avatar Jul 29 '21 09:07 ultrasecreth

Right! I haven't thought of that :) Thanks.

You can keep this issue open if you'd like to improve the error message, otherwise, feel free to close.

Krever avatar Jul 29 '21 12:07 Krever