Ricardo Souza
Ricardo Souza

Reputation: 16468

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.

Upvotes: 1

Views: 490

Answers (2)

Mario Galic
Mario Galic

Reputation: 48430

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.

Upvotes: 2

Mike Slinn
Mike Slinn

Reputation: 8417

You could define Playground this way:

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

Or you could define greeting this way:

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

The explanation for this odd behavior of Scala 2.x is that App features lazy loading. This has been problematic in the past for many others. Scala 3 will change that.

For Scala 2, your safest option is to move your object references outside the body of App, for example:

class Playground {
  val greeting = "Hello Scala"
  println(greeting)
}

object Playground extends App {
  new Playground
}

Upvotes: 3

Related Questions