-3
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.

Ashwin Sreekumar
  • 121
  • 1
  • 12
  • 1
    You need to tell the mock what to do when called, otherwise it obviously has no choice other than returning `null`. – cchantep Jul 25 '22 at 23:29
  • val service1Mock = mock[Service1] Mocking the service itself is failing. Why would mock itself fail ? – Ashwin Sreekumar Jul 26 '22 at 02:22
  • I don't quite know how `@Inject()` works (never in 30 years of experience felt a need to find out :) ), but it looks like it is injecting the actual implementation into `TestService`, not the class you mocked. Sorry, I cannot offer any troubleshooting insight here, because like I said, I don't really know how injection works, and, more importantly, I don't want to know. The best advice I can give you in this situation is to get rid of `@Inject`, and initialize your dependencies explicitly. That will save you _countless hours_ chasing problems like this one in going forward. – Dima Jul 26 '22 at 10:37
  • 1
    Even if I agree that dependency injection is generally counter-productive, but there the issue is not related, and is a pure mocking issue. – cchantep Jul 26 '22 at 12:10
  • Can you show how you use the mock? Instantiating the mock has no reason to fail, at least with Mockito, maybe scalamock works differently. – Gaël J Jul 26 '22 at 17:52
  • @cchantep it is not a "pure mocking" issue: you expect that a mock gets injected into your `TestService`, but instead the real implementation gets injected. So, it is very much related to injection. Try removing `@Inject` and just doing `new TestService(service1Mock)` to see for yourself. – Dima Jul 27 '22 at 10:41
  • Guys pls check my update, ``` class Service1()(service2: Service2) { def url: String = service2.REDIS_URL }``` if we change it to this (changed def to val) then it give a null pointer and that is what my doubt was , how to auto inject all dependencies. looks like one one level of dependencies are being injected. – Ashwin Sreekumar Jul 27 '22 at 20:19

1 Answers1

1

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.

Alin Gabriel Arhip
  • 2,568
  • 1
  • 14
  • 24
  • where are singleton objects in OP's code? – Dima Jul 26 '22 at 10:38
  • ```class Service1()(service2: Service2) { val url: String = service2.REDIS_URL }``` if we change it to this (changed def to val) then it give a null pointer and that is what my doubt was , how to auto inject all dependencies. looks like one one level of dependencies are being injected – Ashwin Sreekumar Jul 27 '22 at 20:14
  • 1
    I have accepted this answer. As pointed out @AlinGabrielArhip the issue was being caused by the limitation of the library. Using mockito I was able to overcome this. Thanks – Ashwin Sreekumar Aug 01 '22 at 11:25