Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NPE when mocking a trait with a NonEmpyString argument #250

Closed
tyrcho opened this issue Jun 8, 2020 · 14 comments
Closed

NPE when mocking a trait with a NonEmpyString argument #250

tyrcho opened this issue Jun 8, 2020 · 14 comments

Comments

@tyrcho
Copy link

tyrcho commented Jun 8, 2020

I've created a very simple project to reproduce the issue : https://github.com/tyrcho/refined-mockito-scala

This line gets a NullPointerException when the test is run

Any workaround to initialize this mock ?

@ultrasecreth
Copy link
Member

I'll take a look

@ultrasecreth
Copy link
Member

The issue seems to be that the refined type is a value class, for which you need to specify the type when you use an any matcher.
I'm not too familiar with this lib, but could you try doing something like this

when(helloRefined.greet(refineMV[NonEmpty](any[String]))) thenReturn "hello"

the above example doesn't work as it expects a literal, not sure how to dynamically instantiate a non empty string.

if you make it work please update your example so I can see if we can create a something to make the interaction with refined a bit smoother

@tyrcho
Copy link
Author

tyrcho commented Jun 9, 2020

well I really don't understand the exception. I also tested with this line

    when(helloRefined.greet(argThat(f = { _: NonEmptyString => true }))) thenReturn "hello"

and I still get the same NPE. I don't see where mockito is trying to instanciate an NES or what would cause a NPE.

The stack is not helping ...

[info]   java.lang.NullPointerException:
[info]   at com.demo.HelloWorldTest.$anonfun$new$2(HelloWorldTest.scala:18)
[info]   at org.scalatest.OutcomeOf.outcomeOf(OutcomeOf.scala:85)

@tyrcho
Copy link
Author

tyrcho commented Jun 9, 2020

OK, I think I understood by splitting the code

  "refined" should "greet" in {
    val at: NonEmptyString = argThat(f = { _: NonEmptyString => true })
    // the line above fails because NES cannot be instanciated by mockito
    val g: String = helloRefined.greet(at)
    val s: ScalaFirstStubbing[String] =when(g)
    s thenReturn "hello"
    hello.greet("me") shouldBe "hello"
  }

@tyrcho
Copy link
Author

tyrcho commented Jun 9, 2020

#244 gave me an idea ... I tried this code

  "refined" should "greet" in {
    when(helloRefined.greet(eqTo(NonEmptyString("me")))) thenReturn "hello"
    when(helloRefined.greet(eqTo(NonEmptyString("you")))) thenReturn "bye"
    hello.greet("me") shouldBe "hello"
    hello.greet("you") shouldBe "bye"
  }

But the last test fails, the mock always return "hello" :-(

@ultrasecreth
Copy link
Member

ultrasecreth commented Jun 9, 2020

I think there are 2 problems here

  1. the refined class is a value class, in those cases, you must provide the type explicitly to the any matcher so it can wrap itself in said vale class. The old school way was to manually do ValueClass(any), now the macro does that automatically when you write any[ValueClass].
    A problem with Refined is that its value clases have the constructor private, so the macro can't instantiate them (the macro actually expands to new ValueClass(any)

  2. The any matcher returns null when the runtime calls it to substitute the params before calling the actual method in the mock, which is a problem here, because even if the matcher was smart enough to return "" instead of null, you're still using it with a refined type that should fail in that scenario.

I just saw your last comment (I was about to suggest using an actual value), in the code you posted and in that example you are mocking a refined class but then asserting on a regular one :P
You stub on helloRefined.greet but then call hello.greet
Also you shouldn't need the eqTo, you can pass the value directly like a regular invocation.

Please update the code in the repo you posted so I can check it out, I tried copying that but there are some missing implicits I don't know where to get from

@tyrcho
Copy link
Author

tyrcho commented Jun 9, 2020

OK, first good news is that we have a workaround when comparing to an actual value:

import eu.timepit.refined.auto._

  "refined" should "match and greet" in {
    when(helloRefined.greet(NonEmptyString("me"))) thenReturn "hello"
    when(helloRefined.greet(NonEmptyString("you"))) thenReturn "bye"
    helloRefined.greet("me") shouldBe "hello"
    helloRefined.greet("you") shouldBe "bye"
  }

I pushed this version of the code @sallareznov in https://github.com/tyrcho/refined-mockito-scala

@ultrasecreth
Copy link
Member

yeah, I have the feeling that the any matcher with refined types is not gonna work, mainly cause it's quite probable that any default value that it may return could be against what the refined type is checking.
the eq matcher and the like return the value you are expressing, so the type validation will run against it, and hence it should work.

@tyrcho
Copy link
Author

tyrcho commented Jun 9, 2020

for the initial issue, how could we "tell mockito" to use another way to instanciate with NonEmptyString.unsafeApply instead of using the constructor ?

I don't really understand why mockito needs to instanciate the argument type in some cases. I also tried with argThat but same NPE.

@tyrcho
Copy link
Author

tyrcho commented Jun 9, 2020

yeah, I have the feeling that the any matcher with refined types is not gonna work, mainly cause it's quite probable that any default value that it may return could be against what the refined type is checking.

Could we somehow tell the matcher to use a default value like "non empty" instead of null or "" ?

@ultrasecreth
Copy link
Member

the thing is that mockito is a Java lib, so its internals don't know about the value types as they don't really exist in Java land, it just deals with the wrapped type, that's why the matcher has to go inside the value class instance.

@ultrasecreth
Copy link
Member

yeah, I have the feeling that the any matcher with refined types is not gonna work, mainly cause it's quite probable that any default value that it may return could be against what the refined type is checking.

Could we somehow tell the matcher to use a default value like "non empty" instead of null or "" ?

Yes, I guess we could have a default, or an implicit that defines that, I'll take a look after work

@ultrasecreth
Copy link
Member

Btw, we should be having this discussion in Gitter, it would be much more fluent ;)

@tyrcho
Copy link
Author

tyrcho commented Jun 9, 2020

conclusion of the discussion on gitter:

there is a workaround like this one

"refined" should "greet" in {
    val helloRefined = mock[RefinedHelloWorld]
    when(helloRefined.greet(star)) thenReturn "hello"
    helloRefined.greet("me") shouldBe "hello"
    helloRefined.greet("you") shouldBe "hello"
  }

  implicit val defaultNes: NonEmptyString = NonEmptyString(" ")
}

object Util {
  def star[T](implicit default: T): T = {
    ArgumentMatchers.argThat((_: T) => true)
    default
  }
}

sample project is updated @ https://github.com/tyrcho/refined-mockito-scala

I let you guys decide if you want to implement sth to make it easier within the library (would be great !)

ultrasecreth added a commit that referenced this issue Aug 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants