Scala中依赖方法计算仅执行一次的测试方案问询
Hey there! As a fellow Scala learner, I get how tricky it can be to test lazy val behavior—especially making sure those computations only run once when needed. Let's break down how to turn your print-statement checks into proper automated tests using ScalaTest and Mockito, which is perfect for verifying method call counts.
First, make sure you have the right dependencies in your build.sbt to use ScalaTest and Mockito-Scala (it’s more Scala-friendly than plain Mockito):
libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.2.15" % Test, "org.mockito" %% "mockito-scala" % "1.17.12" % Test )
We’ll use a spy for your Ops class. A spy lets us keep all the original logic of your methods (so square still calculates squares, sum still sums, etc.) but tracks how many times each method is called—exactly what we need to verify your lazy val behavior.
Here’s a complete test class covering all three of your scenarios:
import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.mockito.MockitoSugar._ class OpsSpec extends AnyFlatSpec with Matchers with MockitoSugar { private val testNums = List(2, 4, 4) // Our test input // Test 1: Calling y returns 36, square and sum run once each "Ops.y" should "return 36 and trigger square and sum exactly once" in { val ops = spy(new Ops(testNums)) val result = ops.y // Check the output is correct result shouldBe 36 // Verify square was called once with our test list verify(ops, times(1)).square(testNums) // Verify sum was called once with the result of x verify(ops, times(1)).sum(ops.x) // Make sure sqrt never gets called here (since we didn't access z) verify(ops, never).sqrt(any[Int]) } // Test 2: Calling x returns the right list, square doesn't re-run on re-access "Ops.x" should "return List(4,16,16) and not re-execute square when accessed multiple times" in { val ops = spy(new Ops(testNums)) // Access x twice val firstX = ops.x val secondX = ops.x // Check the output is correct firstX shouldBe List(4, 16, 16) // Verify both calls return the exact same instance (no re-computation) firstX shouldBe theSameInstanceAs(secondX) // Confirm square was only called once verify(ops, times(1)).square(testNums) } // Test 3: Calling z returns 6, sqrt runs once, square/sum don't re-run "Ops.z" should "return 6.0 and trigger sqrt once, without re-running square or sum" in { val ops = spy(new Ops(testNums)) // First, access y to trigger square and sum once ops.y // Now access z val zResult = ops.z // Check the output is correct zResult shouldBe 6.0 // Verify sqrt was called once with the sum (36) verify(ops, times(1)).sqrt(36) // Confirm square and sum were only called once total (from accessing y) verify(ops, times(1)).square(testNums) verify(ops, times(1)).sum(ops.x) } }
Quick breakdown of the test logic:
- Spy: Wrapping
new Ops(testNums)inspy(...)lets us track method calls without altering the original method behavior. - verify: Mockito’s way to check call counts—
times(1)means exactly one call,nevermeans no calls at all. - theSameInstanceAs: In the second test, this confirms both accesses to
xreturn the same object, proving the lazy val wasn’t re-computed.
What if you want to test your original Operations class (without helper methods)?
If you prefer the cleaner Operations class from your first scheme, refactor it to use a trait for calculations. This makes mocking and testing straightforward:
// Extract calculations to a trait trait Calculations { def square(nums: List[Int]): List[Int] = nums.map(n => n * n) def sum(nums: List[Int]): Int = nums.sum def sqrt(num: Int): Double = math.sqrt(num) } // Original class now extends the trait class Operations(nums: List[Int]) extends Calculations { lazy val x: List[Int] = square(nums) lazy val y: Int = sum(x) lazy val z: Double = sqrt(y) }
Then in tests, mock the Calculations trait to track calls while defining expected outputs:
"Operations.z" should "return 6.0 with proper call counts" in { val mockCalc = mock[Calculations] when(mockCalc.square(testNums)).thenReturn(List(4,16,16)) when(mockCalc.sum(List(4,16,16))).thenReturn(36) when(mockCalc.sqrt(36)).thenReturn(6.0) // Create an Operations instance using our mock calculations val ops = new Operations(testNums) { override def square(nums: List[Int]): List[Int] = mockCalc.square(nums) override def sum(nums: List[Int]): Int = mockCalc.sum(nums) override def sqrt(num: Int): Double = mockCalc.sqrt(num) } ops.z shouldBe 6.0 verify(mockCalc, times(1)).square(testNums) verify(mockCalc, times(1)).sum(List(4,16,16)) verify(mockCalc, times(1)).sqrt(36) }
This approach keeps your core class clean while making testability easy.
内容的提问来源于stack exchange,提问作者ssj_100

