You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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) in spy(...) 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, never means no calls at all.
  • theSameInstanceAs: In the second test, this confirms both accesses to x return 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:17:46