Search code examples
scalamockitoscalatestscalamock

Scala test dependent methods used to calculate vals are executed only once


I am new to scala, and I'm trying figure out the best way to test the following process.

I have a class that gets a list of numbers from constructor parameter. The class supports various operations on the list, some operations may depend on the output of other operations. But every option should only perform calculations on demand and should be done at most once. No calculations should be done in the constructor.

Example class definition .
InputList: List[Int] .

x: returns a vector with the square of all elements in InputList .

y: returns the sum of all elements in x .

z: returns the square root of y .

As for class implementation, I think I was able to come up with a fitting solution but now I can't figure out how can I test the calculations of the dependent tree of operations are executed only once.

Class Implementation Approach #1:

class Operations(nums: List[Int]) {
  lazy val x: List[Int] = nums.map(n => n*n)
  lazy val y: Int = x.sum
  lazy val z: Double = scala.math.sqrt(y)
}

This was my first approach which I'm confident will do the job but could not figure out how to properly test it so I decided to add some helper methods to confirm they are being called just ones

Class Implementation Approach #2:

class Ops(nums: List[Int]) {

  def square(numbers: List[Int]): List[Int] = {
    println("calling square function")
    numbers.map(n => n*n)
  }

  def sum(numbers: List[Int]): Int = {
    println("calling sum method")
    numbers.sum
  }

  def sqrt(num: Int): Double = {
    println("calling sqrt method")
    scala.math.sqrt(num)
  }

  lazy val x: Vector[Double] = square(nums)
  lazy val y: Double = sum(x)
  lazy val z: Double = sqrt(y)
}

I can now confirm each dependent method of each method is called just once whenever necessary.

Now how can I write tests for these processes. I've seen a few posts about mockito and looked at the documentation but was not able to find what I was looking for. I looked at the following:

Shows how to test whether a function is called once but then how to test whether other depend functions where called? http://www.scalatest.org/user_guide/testing_with_mock_objects#mockito

Mockito: How to verify a method was called only once with exact parameters ignoring calls to other methods?

Seems promising but I can't figure out the syntax:

https://github.com/mockito/mockito-scala

Example Tests I'd like to perform

var listoperations:Ops = new Ops(List(2,4,4))
listoperations.y // confirms 36 is return, confirms square and sum methods were called just once
listoperations.x // confirms List(4,16,16) and confirms square method was not called
listoperations.z // confirms 6 is returned and sqrt method called once and square and sum methods were not called.

Solution

  • Ok, lets leave the pre-mature optimisation argument for another time.

    Mocks are meant to be used to stub/verify interactions with dependencies of your code (aka other classes), not to check internals of it, so in order to achieve what you want you'd need something like this

    class Ops {
     def square(numbers: List[Int]): List[Int] = numbers.map(n => n*n)
     def sum(numbers: List[Int]): Int = numbers.sum
     def sqrt(num: Int): Double = scala.math.sqrt(num)
    }
    
    class Operations(nums: List[Int])(implicit ops: Ops) {
     lazy val x: List[Int] = ops.square(nums)
     lazy val y: Int = ops.sum(x)
     lazy val z: Double = ops.sqrt(y)
    }
    
    import org.mockito.{ ArgumentMatchersSugar, IdiomaticMockito}
    
    class IdiomaticMockitoTest extends AnyWordSpec with IdiomaticMockito with ArgumentMatchersSugar
      "operations" should {
        "be memoised" in {
          implicit val opsMock = spy(new Ops)
          val testObj = new Operations(List(2, 4, 4))
    
          testObj.x shouldBe List(4, 16, 16)
          testObj.y shouldBe 36
          testObj.y shouldBe 36 //call it again just for the sake of the argument
          testObj.z shouldBe 6 //sqrt(36)
          testObj.z shouldBe 6 //sqrt(36), call it again just for the sake of the argument
    
          opsMock.sum(*) wasCalled once
          opsMock.sqrt(*) wasCalled once
        }
      }
    }
    

    Hope it makes sense, you mentioned you're new to scala, so I didn't wanna go too crazy with implicits so this is a very basic example in which the API of your original Operations class is the same, but it extracts out the heavy lifting to a third party that can be mocked so you can verify the interactions.