Simon
Simon

Reputation: 6462

Play! Scala 2.5 : testing classes injecting cache leads to an error

I'm using for the first time the cache of Play! Scala 2.5. It works well except for the tests.

My test still pass since I don't need the cache but I get this error (and a lot of others telling the same thing):

Unable to provision, see the following errors:

1) Error in custom provider, play.api.cache.EhCacheExistsException: An EhCache instance with name 'play' already exists.

I understand the error but I didn't manage to implement my own version of the cache API (to mock it).

I tried to do what is told on the play mailing list but without success (there is some differences with Play! 2.4 since the module is dependency injected). Any help would be welcome.

Edit: what I have done (and it does not change anything):

My CacheApi version (just for the tests):

class MyCacheApi extends CacheApi {
  lazy val cache = {
    val manager = CacheManager.getInstance()
    manager.addCacheIfAbsent("play")
    manager.getCache("play")
  }

  def set(key: String, value: Any, expiration: Duration = Duration.Inf) = {}

  def remove(key: String) = {}

  def getOrElse[A: ClassTag](key: String, expiration: Duration = Duration.Inf)(orElse: => A): A = {
    get[A](key).getOrElse {
      val value = orElse
      set(key, value, expiration)
      value
    }
  }

  def get[T: ClassTag](key: String): Option[T] = None
}

And in my tests, I use it like this:

lazy val appBuilder = new GuiceApplicationBuilder()
    .in(Mode.Test)
    .overrides(bind[CacheApi].to[MyCacheApi])

lazy val injector = appBuilder.injector()
lazy val cache = new MyCacheApi
lazy val facebookAPI = new FacebookAPI(cache)

But when I'm testing the functions of the class FacebookAPI, the tests pass, but I still have a lot of error messages due to the fact that an EhCache instance with name 'play' already exists...

Upvotes: 8

Views: 4234

Answers (5)

Todor Kolev
Todor Kolev

Reputation: 1482

If you have more than one test suite that uses the same cache, you can explicitly shutdown the CacheManager at the end of each test suite and run them sequentially:

override def afterAll(): Unit = {
  CacheManager.getInstance().shutdown()
}

Upvotes: 2

Deliganli
Deliganli

Reputation: 320

In my version of play(2.6.17) and scala (2.12.6) @GeReinhart 's answer needs small changes.

  • An Inject annotation

  • expiration.toSeconds throws illegal argument exception if expiration is infinite, so a finite check

-

import akka.Done
import javax.inject.Inject
import net.sf.ehcache.Element
import play.api.cache.AsyncCacheApi

import scala.concurrent.duration.Duration
import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.ClassTag

class InMemoryCache @Inject()()(implicit ec: ExecutionContext) extends AsyncCacheApi {

  val cache = scala.collection.mutable.Map[String, Element]()

  def remove(key: String): Future[Done] = Future {
    cache -= key
    Done
  }

  def getOrElseUpdate[A: ClassTag](key: String, expiration: Duration)(orElse: => Future[A]): Future[A] = {
    get[A](key).flatMap {
      case Some(value) => Future.successful(value)
      case None        => orElse.flatMap(value => set(key, value, expiration).map(_ => value))
    }
  }

  def set(key: String, value: Any, expiration: Duration): Future[Done] = Future {
    val element = new Element(key, value)

    if (expiration.isFinite()) {
      element.setTimeToLive(expiration.toSeconds.toInt)
    } else {
      element.setEternal(true)
    }

    cache.put(key, element)
    Done
  }

  def get[T: ClassTag](key: String): Future[Option[T]] = Future {
    cache.get(key).map(_.getObjectValue).asInstanceOf[Option[T]]
  }

  def removeAll(): Future[Done] = Future {
    cache.clear()
    Done
  }

}

In the application builder that needs import below

import play.api.inject.bind

Upvotes: 2

GeReinhart
GeReinhart

Reputation: 423

Having several tests building play application, the only way we found to solve this issue is :

  • to NOT use EhCache for the tests
  • to use a custom in memory AsyncCacheApi for the tests
  • to use EHCache for the production

So in the application.conf used for the tests, and only for tests, disable the default caching :

play.modules.disabled += "play.api.cache.ehcache.EhCacheModule"

Write an implementation of the cache with a map that extends AsyncCacheApi :

package utils

import akka.Done
import net.sf.ehcache.Element
import play.api.cache.AsyncCacheApi

import scala.concurrent.duration.Duration
import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.ClassTag

class InMemoryCache (implicit ec : ExecutionContext) extends AsyncCacheApi {

  val cache = scala.collection.mutable.Map[String, Element]()

  def set(key: String, value: Any, expiration: Duration): Future[Done] = Future {
    val element = new Element(key, value)
    if (expiration == 0) element.setEternal(true)
    element.setTimeToLive(expiration.toSeconds.toInt)
    cache.put(key, element)
    Done
  }

  def remove(key: String): Future[Done] = Future {
    cache -= key
    Done
  }

  def get[T: ClassTag](key: String): Future[Option[T]] = Future {
    cache.get(key).map(_.getObjectValue).asInstanceOf[Option[T]]
  }

  def getOrElseUpdate[A: ClassTag](key: String, expiration: Duration)(orElse: => Future[A]): Future[A] = {
    get[A](key).flatMap {
      case Some(value) => Future.successful(value)
      case None => orElse.flatMap(value => set(key, value, expiration).map(_ => value))
    }
  }

  def removeAll(): Future[Done] = Future {
    cache.clear()
    Done
  }

}

Then on your tests :

val application = new GuiceApplicationBuilder().
                overrides(
                    bind[AsyncCacheApi].toInstance(new utils.InMemoryCache())
                ).build
Play.start(application)

Versions used : Play 2.6.15 & Scala 2.12.4

Upvotes: 3

Simon
Simon

Reputation: 6462

I finally found a solution.

I added in a test.conf file (in the conf folder):

play.cache.bindCaches = ["controller-cache", "document-cache"]

play.cache.createBoundCaches = false

And to make this conf file used in the test I just added in the setting part of my build.sbt the following line:

javaOptions in Test += "-Dconfig.resource=tests.conf"

Let me know if you need more details.

Upvotes: 6

monzonj
monzonj

Reputation: 3709

This is most likely due to the parallel nature of the tests. For instance, I'm using specs2 and If I have two tests that use "WithApplication()" (to fake an application in play 2.5.x) I will get the error about ehcache.

My solution was to run a test after the other. In the case of specs2 just by adding "sequential" in the beginning of the test class. I'm not sure how to do that in "ScalaTestPlus" but you get the point.

Upvotes: 0

Related Questions