scalascalatestscala-2

Why a Scala 2 test assertion on an object's val throws NullPointerException?


While testing Scala code, I've run into a strange NPE while asserting on a value from an object.

Here is the minimal code to reproduce the issue:

main/scala/Playground.scala:

object Playground extends App {
  val greeting = "Hello Scala"
  println(greeting)
}

test/scala/PlaygroundSpec.scala:

import org.scalatest.wordspec._

class PlaygroundSpec extends AnyWordSpec {
  "The playground code" should {
    "say hello" in {
      assert(Playground.greeting.contains("Hello")) // Throws NPE because greeting is null. How???
    }
  }
}

The sample program runs just fine and prints "Hello Scala", but the test throws a NullPointerException on the assertion line, because greeting is null.

How could greeting be null if it is initialized with a string constant?

Note: Adding lazy to the val declaration makes it work and the test passes.


Solution

  • In Scala 2 App extends DelayedInit, so compiler magically rewrites initialisation code such that the initialisation of fields is moved to delayedInit method, for example,

    object Playground extends App {
      val greeting = "Hello Scala"
      println(greeting)
    }
    

    becomes something like

    object Playground extends App {
      private var greeting: String = null
      def greeting(): String = greeting
    
      def delayedInit(): Unit = {   
        greeting = "Hello Scala"
        println(greeting()) 
      }
    
      def main(args: Array[String]) = {
        // indirectly call delayedInit
        ...
      }
    }
    

    Now we can see

    assert(Playground.greeting.contains("Hello"))
    

    becomes

    assert(null.contains("Hello"))
    

    as delayedInit method did not get called. To prove the point observe how the following works

    Playground.main(Array.empty) // delayedInit gets indirectly called
    assert(Playground.greeting.contains("Hello")) // ok
    

    Adding lazy to the val declaration makes it work and the test passes.

    This works because lazy val greeting effectively turns the field into a method which moves it out of the initialisation code so it does not become part of delayedInit.

    Clearly this is confusing so Scala 3 Dropped: Delayedinit.