3

I'm initializing a companion object for one of my scala test suites. One of the fields in this companion object is lazy evaluated and uses some of the fields in the test suite for initialization. Something like:

class SomeClassSpec extends WordSpec with Matchers with OneInstancePerTest {
    lazy val someFieldTheCompanionObjectNeeds = "some_field_I_need"
    "my test class" should {
        "do something interesting" when {
            "I tell it to" in {
                //a bunch of test code using the SomeTestClassCompanionObject.someConfigurationINeed field.
            }
        }
    }
}

object SomeTestClassCompanionObject extends SomeClassSpec {
    lazy val someConfigurationINeed = Config(SomeTestClass.someFieldTheCompanionObjectNeeds)
}

Don't ask. I know this is bad practice, but it has to be done, and this is largely unrelated to my question.

What I noticed here was, my SomeTestClassCompanionObject.someConfigurationINeed field was not initialized if I tried to use it inside the when block of the test, however it is initialized inside the in block. My question is: what actually differentiates each of the should, when, in scopes in Wordspec? I was under the impression that these were simply logical differentiations, but this test shows that different things are initialized at different times in the underlying "static" block of the JVM code.

Does anyone have any further reading or links to the Wordspec documentation that explains what's going on here?

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
Jackson Kelley
  • 412
  • 5
  • 13

1 Answers1

2

@BogdanVakulenko shows how the following design

class SomeClassSpec {
  SomeTestClassCompanionObject.someConfigurationINeed // NullPointerException or StackOverflowError because calling child's constructor which in turn calls parent's constructor
}

object SomeTestClassCompanionObject extends SomeClassSpec {
  lazy val someConfigurationINeed = ??
}

fails because calling child's constructor from parent's constructor results in a cycle. This very scenario occurs with should and when

class SomeClassSpec {
  "my test class" should { 
    SomeTestClassCompanionObject.someConfigurationINeed // error
  }

  "do something interesting" when {
    SomeTestClassCompanionObject.someConfigurationINeed // error
  }
}

because despite them taking pass-by-name parameter f which is evaluated only when used

def should(right: => Unit)
def when(f: => Unit)

they result in a call to registerNestedBranch which does indeed evaluate f thus triggering the cycle

  def registerNestedBranch(description: String, childPrefix: Option[String], fun: => Unit, registrationClosedMessageFun: => String, sourceFile: String, methodName: String, stackDepth: Int, adjustment: Int, location: Option[Location], pos: Option[source.Position]): Unit = {
    ... 
    try {
      fun // Execute the function
    }
    ...
}

On the other hand, the cycle does not occur with in

class SomeClassSpec {
  "I tell it to" in {
    SomeTestClassCompanionObject.someConfigurationINeed // ok
  }
}

which also takes f by-name

def in(f: => Any /* Assertion */)

because it results in call to registerTest which just registers function value f for execution but at no point does f get evaluated as it is passed down for registration. Then separate Runner object actually runs f but at that point call to SomeTestClassCompanionObject.someConfigurationINeed is executed outside SomeClassSpec's constructor, hence no cycle is triggered.

Mario Galic
  • 47,285
  • 6
  • 56
  • 98