NullPointerExcection when mocking Silhouette in Mockito


#1

I am getting Null Pointer error when unit testing controller with Silhouette. I might not be mocking Silhouette correctly. Could you please tell me how to use Mockito and Silhouette.

Following is a snippet of a controller I want to unit-test

class UserController @Inject()(userRepo: UsersRepository,cc: ControllerComponents, silhouette: Silhouette[JWTEnv])(implicit exec: ExecutionContext) extends AbstractController(cc){ ... }

I only want to test that the controller returns an error when it gets a request without json body. Thus I don’t need Silhouette and I want to mock it. But I am getting null pointer error.

Following is the way I have written my unit test case is

   class UserControllerUnitSpec extends PlaySpec with MockitoSugar {
    
      "User signup request with non-JSON body" should {
    
        "return  400 (Bad Request) and the validation text 'Incorrect body type. Body type must be JSON'" in {
    
          val email = "d@d.com"
          val loginInfo = LoginInfo(CredentialsProvider.ID, email);
          val passwordInfo = PasswordInfo("someHasher","somePassword",Some("someSalt"))
          val internalUserProfile = InternalUserProfile(loginInfo,true,Some(passwordInfo))
          val externalUserProfile = ExternalUserProfile(email,"d","d",Some("somePassword"))
          val userProfile = UserProfile(Some(internalUserProfile),externalUserProfile)
          val user = User(UUID.randomUUID(),userProfile)
    
          println("testing with mocked User value",user);
    
          val mockUserRepository = mock[UsersRepository]
          when(mockUserRepository.findUser(loginInfo)).thenReturn(Future(Some(user)))
          when(mockUserRepository.saveUser(user)).thenReturn(Future(Some(user)))
    
          val mockSilhouette = mock[Silhouette[JWTEnv]] //I am probably not doing this correctly
          val mockControllerComponents = mock[ControllerComponents] //I am not sure if this is correct either
          val controller = new UserController(mockUserRepository,mockControllerComponents,mockSilhouette)
    
          val result:Future[Result] = controller.signupUser(FakeRequest())
          (result.map(response => {
            println("response: ",response)
            response mustBe BadRequest
          }))
        }
      }
    }

I am getting error

`java.lang.NullPointerException was thrown.`
`java.lang.NullPointerException`
`	at controllers.UserController.signupUser(UserController.scala:59)`

which corresponds to code def signupUser = silhouette.UserAwareAction.async{ implicit request => {


#2

You should use mock[YourClass].smart, to show more info about mocking errors


#3

Do you mean like val mockUserRepository = mock[UsersRepository].smart? I tried this but the IDE cannot find smart. I am reading the testing documentation. I changed my implementation and created a FakeEnvironment as follows:

implicit val env = FakeEnvironment[JWTEnv](Seq(loginInfo->user))

But how to I create silhouette stack using the fake environment? The documentation says that I could use SilhouetterProvider. I thought of doing the following but now I’ll have to create secure, unsecure and useraware actions. I cannot do that because they require instance of messageApi and I dont have that in my spec.

val mockSilhouette = new SilhouetteProvider[JWTEnv](env,)


#4

Do you mean like val mockUserRepository = mock[UsersRepository].smart?

Does it compile or is it only an IDE issue?

But how to I create silhouette stack using the fake environment?

Creating the Silhouette stack by hand is very complex because it has a huge dependency tree. You already have this stuff wired in your Guice conf. The only thing you must replace is your production ready environment with your fake environment.

Please have a look at this test. This is a working test. Please also have a look at the AuthSpecification.scala


#5

The code with smart doesn’t compile.

val mockUserRepository = mock[UsersRepository].smart

It seems mock creates instance of UserRepository because the compilation error is

Error:(43, 54) value smart is not a member of repository.UsersRepository
      val mockUserRepository = mock[UsersRepository].smart

I cannot change from Compile time DI to runtime DI. I was thinking of doing something like the following but I couldn’t find any fake implementations of secure, unsecure and useraware actions. the default ones use messageApi which is not available in my spec. So I don’t know how to test my controller.

      implicit val env = FakeEnvironment[JWTEnv](Seq(loginInfo->user))
      val mockSilhouette = new SilhouetteProvider[JWTEnv](env,...)

#6

In the testing section, the last statement says

Test default Play actions
Typically Silhouette authentication code is implemented inside default Play actions. To test such actions you don’t need specific helper classes. Here you could use Mockito to mock the Silhouette instances or other related testing tools.

Could you elaborate how I should use Mockito to mock the Silhouette instances? Say someone wants to use Silhouette in their controller but hasn’t yet implemented Silhouette functionality in the controller. But the controller has other functionality which needs to be unit tested (like in my case, a non-json should give error). How should someone test this?


#7

smart is probably not working because it is a method of MockitoSugar, not Mockito. See http://doc.scalatest.org/3.0.1/index.html#org.scalatest.mockito.MockitoSugar$


#8

So I removed MockitoSugar and am now using Mockito. I changed the mock code to

val mockSilhouette = mock(classOf[Silhouette[JWTEnv]],RETURNS_SMART_NULLS)

I see the following error
Mockito cannot mock this class: class com.mohiva.play.silhouette.api.actions.UserAwareActionBuilder.


#9

I thought you use Guice because you use the @Inject annotation in your controller example in the first post.

With compile time DI you probably have a component/module that wires your instances. So for testing you need that component/module too, expecting that your production ready environment is overridden with your testing environment. Silhouette provides some compile time DI components for wiring actions. You probably use this components already.

smart is probably not working because it is a method of MockitoSugar, not Mockito.

Yes, this might be the case.

In the testing section, the last statement says

Good, question. I think I would say that when you test normal Play actions, you do not need the Silhouette testkit. So if you have a controller with a Silhouette stack dependency, in this case you can mock the Silhouette instance, because it never gets used. This section in the doc exists since version 2 and in the meantime a lot has changed.


#10

Yes, as noted before. You should not mock these classes.


#11

I am not sure about this bit. I just changed my controller code to Action.async{ instead of silhouette.UserAwareAction.async{ but I still get the null pointer! So mocking of Silhouette doesn’t seem to be working (irrespective of whether I am using Silhouette in controller or not)


#12

In your controller you typically inject a Silhouette instance. So if you write:

val silhouette = mock[Silhouette[Env]]

Then this should work if you pass this mock to your controller to test a default Play action.

But as noted, you shouldn’t mock any Silhouette related class to test a Silhouette action. This isn’t needed. Do you mock the complete Play stack or the Play actions to test code inside an action? I don’t think so. You should only mock your user defined classes and use the FakeEnvironment from play-silhouette-teskit to override your production environment.


#13

I have an interesting observation. I am still evaluating this but maybe you could give some advice. I created a new simple method in my controller which just prints the instance of Silhouette passed to it.

def someMethod() = { println("called some method, ",silhouette)}

Now when I tested it,

     val mockUserRepository = mock(classOf[UsersRepository])   
      val mockSilhouette = mock(classOf[Silhouette[JWTEnv]])
      val mockControllerComponents = mock(classOf[ControllerComponents])
  val controller = new UserController(mockUserRepository,mockControllerComponents,mockSilhouette)
  
  println("sending request",request)
  val result = controller.someMethod()
  result mustBe Unit

The program didnt crash and I got this failure

(called some method, ,Mock for Silhouette, hashCode: 980533504)

<(), the Unit value> was not equal to object scala.Unit

So I am printing Silhoutte's mock and it didnt crash.

I suspect the issue could be with the request in

def signupUser = Action.async{
    implicit request => {

I suspect so because the stacktrace from previous tests point to implicit request line. But I don’t know what could be wrong in this because I am using FakeRequest like so val request = FakeRequest("POST", "/ws/users/signup").withJsonBody(Json.parse("""{"bad": "field"}"""))


#14

This worked.

class UserControllerUnitSpec extends PlaySpec /*with MockitoSugar*/ {

  "User signup request with non-JSON body" should {

    "return  400 (Bad Request) and the validation text 'Incorrect body type. Body type must be JSON'" in {

      val email = "d@d.com"
      val loginInfo = LoginInfo(CredentialsProvider.ID, email);
      val passwordInfo = PasswordInfo("someHasher","somePassword",Some("someSalt"))
      val internalUserProfile = InternalUserProfile(loginInfo,true,Some(passwordInfo))
      val externalUserProfile = ExternalUserProfile(email,"d","d",Some("somePassword"))
      val userProfile = UserProfile(Some(internalUserProfile),externalUserProfile)
      val user = User(UUID.randomUUID(),userProfile)

      println("testing with mocked User value",user);

      val mockUserRepository = mock(classOf[UsersRepository])
      // when(mockUserRepository.findUser(loginInfo)).thenReturn(Future(Some(user)))
     // when(mockUserRepository.saveUser(user)).thenReturn(Future(Some(user)))

     // val mockSilhouette = mock(classOf[Silhouette[JWTEnv]])
      val mockControllerComponents = Helpers.stubControllerComponents()//mock(classOf[ControllerComponents])
      /*
      The controller needs Silhouette. Using Silhouette's test kit to create fake instances.
      If you would like to test this controller, you must provide an environment that can handle your Identity and Authenticator implementation.
      For this case Silhouette provides a FakeEnvironment which automatically sets up all components needed to test your specific actions.

      You must only specify one or more LoginInfo -> Identity pairs that should be returned by calling request.identity in your action and
      the authenticator instance that tracks this user.
       */

      //User extends Identity trait
      /*
      Under the hood, the environment instantiates a FakeIdentityService which stores your given identities and returns it if needed.
      It instantiates also the appropriate AuthenticatorService based on your defined Authenticator type. All Authenticator services are real
      service instances set up with their default values and dependencies.
       */
      implicit val sys = ActorSystem("MyTest")
      implicit val mat = ActorMaterializer()
      implicit val env = FakeEnvironment[JWTEnv](Seq(loginInfo->user))
      val defaultParser = new mvc.BodyParsers.Default()
      val securedAction = new DefaultSecuredAction(new DefaultSecuredRequestHandler(new DefaultSecuredErrorHandler(stubMessagesApi())),defaultParser)
      val unsecuredAction = new DefaultUnsecuredAction(new DefaultUnsecuredRequestHandler(new DefaultUnsecuredErrorHandler(stubMessagesApi())),defaultParser)
      val userAware = new DefaultUserAwareAction(new DefaultUserAwareRequestHandler(),defaultParser)
      val mockSilhouette = new SilhouetteProvider[JWTEnv](env,securedAction,unsecuredAction,userAware)

      val controller = new UserController(mockUserRepository,mockControllerComponents,mockSilhouette)
      
      val request = FakeRequest("POST","ws/users/signup")
      println("sending request",request)
      //val result = controller.someMethod()
      val result:Future[Result] = controller.signupUser(request)
      
      status(result) mustBe BAD_REQUEST
      
    }
  }
}

Silhouette controller - pure unit testing