Search code examples
scalaunit-testingplayframeworkscalamock

Dependency mocking in scala


class Service1 @Inject()(service2: Service2) {
  val url = service2.REDIS_URL
}
class TestService @Inject()(service1: Service1) {
  def foo() => {}
}

I have the above 2 classes. I need to test TestService.foo. Following is the code that I am trying but its not working.

class TestServiceTest extends org.scalatest.AsyncFunSuite with MockFactory  {
   val service1Mock = mock[Service1]
....
....
}

While initiating the test cases service2.REDIS_URL fails with null pointer error. I am unable to find any answers in the scala mock documentation about how to properly mock services/singleton objects.

Update:

class Service2  @Inject()(){
  val REDIS_URL = "some constant"
}

class Service1  @Inject()(service2: Service2){
  val redisStr = service2.REDIS_URL
  def getUrl = redisStr
}

class TestService @Inject()(service1: Service1){
  def foo() = service1.getUrl
}


it should "test properly" in {
    val mocks1  = mock[Service1]
}

This is not working

but if we change Service1 to

class Service1 @Inject()()(service2: Service2) {
  def url = service2.REDIS_URL
}

it works.

But,

class Service1 @Inject()()(service2: Service2) {
  def url = service2.REDIS_URL
  
  config.useSingleServer()
        .setAddress(REDIS_URL)
}

Again fails

This is due to service2 being null while the Mock is generated. This is very weird that the class is run while creating the Mock in ScalaTest and it finds service2 to be null causing NPE.


Solution

  • No, you cannot mock singleton objects in Scala. But I don't see any in your code. And you mock services just like any other class in Scala.

    I am not sure I understand what your actual problem is, but I will try explain the best I can what I understood so far. As someone already said you have to tell your mock what calls to mock, otherwise of course it has no choice but to return null to whatever tries dereferencing it.

    By mixing in MockFactory this means you are using the mock method of ScalaMock. A known limitation of ScalaMock is that it does not support mocking of val fields. This is because mocks are generated using macros as anonymous subclasses of the class to mock. But the Scala compiler does not allow overriding of val fields in subclasses, because val fields are immutable.

    So there is no way you can mock service1.url, as long as url remains a val. A quick fix is converting the url into a def, so you can then mock the call to the method url and that should solve your null pointer issue. Here's that idea in action:

    class Service1 @Inject() (service2: Service2) {
      def url: String = service2.REDIS_URL
    }
    
    class TestService @Inject() (service1: Service1) {
      def foo(): String = "this is " + service1.url
    }
    
    // ...
    
    import org.scalamock.scalatest.MockFactory
    import org.scalatest.matchers.should.Matchers
    import org.scalatest.wordspec.AnyWordSpec
    
    class ProgramTest extends AnyWordSpec with Matchers with MockFactory {
        "mock Service1 url " in {
          val service1Mock  = mock[Service1]
          val mytestService = new TestService(service1Mock)
        
          (service1Mock.url _).expects().returns("somethingelse")
          val res = mytestService.foo()
    
          res shouldBe "this is somethingelse"              // true
        }
    }
    

    This works. No nulls here. If for some reason, you don't want to change the url into a def, a better alternative is to switch to mockito-scala because that can mock fields as well. You don't need ScalaMock for this.

    If I understood correctly and your mock of Service1 is still failing with ScalaMock even after changing url to def for some unknown reason, then that's one more reason to switch to mockito-scala. I could not reproduce your null pointer using it. First import this:

    libraryDependencies += "org.mockito"       %% "mockito-scala"   % "1.17.7"   % Test
    

    I tested TestService.foo as follows:

    class Service1 @Inject() (service2: Service2) {
      val url: String = service2.REDIS_URL
    }
    
    class TestService @Inject() (service1: Service1) {
      def foo(): String = "this is " + service1.url
    }
    
    // ...
    
    import org.mockito.MockitoSugar
    import org.scalatest.matchers.should.Matchers
    import org.scalatest.wordspec.AnyWordSpec
    
    class ProgramTest extends AnyWordSpec with MockitoSugar with Matchers {
      "mock Service1 url " in {
        val service1Mock  = mock[Service1]
        val mytestService = new TestService(service1Mock)
    
        when(service1Mock.url).thenReturn("somethingelse")
        val res = mytestService.foo()
    
        res shouldBe "this is somethingelse"              // true
      }
    }
    

    And it worked as expected.