Testing with Specs2 and Play ReactiveMongo

Are you building a Scala PlayFramework application with ReactiveMongo?
Are you testing your application with Specs2?
Are some of your tests failing with the dreaded, “Can’t get ClosableLazy value after it has been closed”?
Do those same tests pass, if they are run individually?

If so, this post is for you!

Note: A full stack trace for the dreaded ClosableLazy can be found in the Appendix of this post.

In this post we will investigate:

  • How and why the dreaded ClosableLazy issue occurs
  • How to ensure this issue does not occur

Let’s get started!

First, Why are some of your tests failing with, “Can’t get ClosableLazy value after it has been closed”?

The cause of this problem is usually due to the fact that as each Specs2 test is completes, the MongoDB database connection is reset. Thus, any subsequent test that holds a reference the closed database connection will result in the dreaded ClosableLazy error.

So, how do we resolve this?
Ensure that our application code never holds a reference to a MongoDb instance that has been closed. This seems complicated, but it’s not.

There are two key aspects required to meet the above criteria:

  1. Avoid using val or lazy val for database resources
  2. (Re)Initialize all database references on application start.

1. Avoid using val or lazy val for database resources

This one is simple. Do not reference db resources with val or lazy val, instead always use def.

    def collection = db.collection[JSONCollection]("mycollection")

2. (Re)Initialize all database references on startup
As mentioned earlier, Specs2 resets the database connection with each completed test, thus we need to ensure that our application never holds a stale reference to a database resource. Criterion 1. is essential to this end, however, on its own, it is not always enough, hence we must reset database reference on application start.

The PlayFramework provides a GlobalSettings object which can be extended to inject custom logic on startup. We will utilize functionality to reset our database references.

object Global extends GlobalSettings {
  override def onStart(app: Application) {
    // Logic to Reset stale references to MongoDb resources
  }
}

Clearly, the key here is to come up with the logic necessary to reset any stale references to our MongoDb resources. Luckily, there are a many ways to do this. Let’s explore one possible method. We will create a new MongoController. The MongoController provides a reference to our MongoDb as defined in application.conf. This is great, but as we know, Specs2 will close the db connection after each test, thus we need some way to reinitialize the MongoController, then update any stale db references.

First, we create a MongoController, in this case a class called DbResources along with a companion object

class DbResources private() extends Controller with MongoController {
}

object DbResources {
  var app : DbResources = null;

  def apply() = {
    app = new DbResources
    app
  }

  def myCollection = app.db.collection[JSONCollection]("mycollection")
  def db = app.db
}

Notice that the DbResources class extends MongoController and thus now has a reference to our MongoDb, db. The DbResources object contains an apply method. In this case, apply creates a new instance of DbResources. Creating the new instance effectively causes our MongoController to create a new db connection. Notice, also, that the db resources exposed by DbResources i.e. myCollection and db are all referenced through app i.e. the current DbResources instance. Now, anytime DbResources.applyis invoked, all references to its database resources are updated.

Essentially, to solve the problem we need to invoke DbResources.apply on startup.
Here’s how:

object Global extends GlobalSettings {
  override def onStart(app: Application) {
    DbResources() // invoke the apply method on DbResources
  }
}

Awesome! Problem solved.

Create and Run a Specs2 test

To test the implementation, create a Test Specificaiton.
For example:

@RunWith(classOf[JUnitRunner])
class MyCoolControllerSpec extends Specification {

  "A POST to MyCoolController" should {
    "response with name equal to cool!"  in new WithApplication() {
      val result = route(FakeRequest(POST, "/cool", FakeHeaders(), request)).get
      status(result) must equalTo(OK)
      (json \ "name").as[String] must beEqualTo("Cool!")
    }
  }
  
  "A GET to MyCoolController" should {
    "response with name equal to cool!"  in new WithApplication() {
      val result = route(FakeRequest(GET, "/cool")).get
      status(result) must equalTo(OK)
      (json \ "name").as[String] must beEqualTo("Cool!")
    }
  }
}

Run the test, and voila, no more “Can’t get ClosableLazy value after it has been closed”?

Thanks!

Appendix:

The dreaded error typically manifests itself in the following stacktrace.

[info] application - ReactiveMongoPlugin stops, closing connections...
[warn] play - Error stopping plugin
java.lang.IllegalStateException: Can't get ClosableLazy value after it has been closed
at play.core.ClosableLazy.get(ClosableLazy.scala:49) ~[play_2.11-2.3.7.jar:2.3.7]
at play.api.libs.concurrent.AkkaPlugin.applicationSystem(Akka.scala:71) ~[play_2.11-2.3.7.jar:2.3.7]
at play.api.libs.concurrent.Akka$$anonfun$system$1.apply(Akka.scala:29) ~[play_2.11-2.3.7.jar:2.3.7]
at play.api.libs.concurrent.Akka$$anonfun$system$1.apply(Akka.scala:29) ~[play_2.11-2.3.7.jar:2.3.7]
at scala.Option.map(Option.scala:145) [scala-library-2.11.4.jar:na]

You may also like...

Leave a Reply