Home > Software design >  Mocking scala object called by under another object
Mocking scala object called by under another object

Time:09-01

I am trying to write unit test for a function under object1.

object Object1 {
  def main(sysArgs: Array[String]): Unit = {
     val inputDF: DataFrame = UtilObject.getInput()
  }
}

object UtilObject {
  def getInput(){
   ...
  }
}

To write Unit test, I am using MockitoSugar.

  "object1Main" should "should make correct calls" in {
    val inputArgs = Array("abc")
    val util = mock[UtilObject.type]

    when(util.getInput().thenReturn(inputData))
    
    Object1.main(inputArgs)
  }

While running the test, it doesn't consider the util mock and just execute the getInput() function.

I think I am missing some sort of injection here. Any ideas?

Thanks in advance!

CodePudding user response:

Mocking Scala objects should be impossible conceptually speaking. An object in Scala is a pure singleton. That means there can only be one member of that type at any time.

mockito-scala can mock Scala objects via reflection. I'll use a result type of String, instead of a DataFrame, but the idea is the same:

  object UtilObject {
    def getInput(): String = {
      // ...
      "done"
    }
  }

  object Object1 {
    def main(sysArgs: Array[String]): String = {
      val inputDF: String = UtilObject.getInput()
      inputDF
    }
  }

  // in test file:
  "object1Main" should {
    "should make correct calls" in {
      val inputArgs = Array("abc")

      withObjectMocked[UtilObject.type] {
        UtilObject.getInput() returns "mocked!"

        Object1.main(inputArgs) shouldBe "mocked!"
      }

      Object1.main(inputArgs) shouldBe "done"
    }
  }

This mocks the singleton's method only inside the block of withObjectMocked.

Usually such powerful techniques often tend to be overused or misused, so I don't generally recommend them, unless the design cannot be refactored.

Luckily, yours can: the easiest way is to use Dependency Injection with a class or a function. For DI with a class you require to convert the object being mocked into a class:

  class UtilObject {
    def getInput(): String = {
      // ...
      "done"
    }
  }

  object Object1 {
    def main(sysArgs: Array[String], ut: UtilObject): String = {
      val inputDF: String = ut.getInput()
      inputDF
    }
  }

  // in test file:
  "object1Main" should {
    "should make correct calls" in {
      val inputArgs = Array("abc")
      val util      = mock[UtilObject]

      when(util.getInput()).thenReturn("mocked!")

      Object1.main(inputArgs, util) shouldBe "mocked!"
    }
  }

For DI with a function you need to lift the method you want to mock into a function:

  object UtilObject {
    def getInput(): String = {
      // ...
      "done"
    }
  }

  object Object1 {
    def main(sysArgs: Array[String], f: () => String = UtilObject.getInput): String = {
      val inputDF: String = f()
      inputDF
    }
  }

  // in test file:
  "object1Main" should {
    "should make correct calls" in {
      val inputArgs = Array("abc")
      val f         = mock[() => String]

      when(f()).thenReturn("mocked!")

      Object1.main(inputArgs, f) shouldBe "mocked!"
    }
  }

Since the function takes no arguments, you can convert it into a by-name parameter. I'll leave that to you.

Another way is to create a trait with the method you want to mock and extend that with the object. But Object1 requires being a class and have a reference to the object being mocked:

  object UtilObject extends Utils {
    def getInput(): String = {
      // ...
      "done"
    }
  }

  trait Utils {
    def getInput(): String
  }

  class Object1 {
    val uo: Utils = UtilObject
    def main(sysArgs: Array[String]): String = {
      val inputDF: String = uo.getInput()
      inputDF
    }
  }

  // in test file: 
  "object1Main" should {
    "should make correct calls" in {
      val classUnderTest = new Object1 {
        override val uo = mock[Utils]
      }
      val inputArgs = Array("abc")

      when(classUnderTest.uo.getInput()).thenReturn("mocked!")

      classUnderTest.main(inputArgs) shouldBe "mocked!"
    }
  }
  • Related