Search code examples
scaladependency-injectionakkaapplication-loader

How can a custom ApplicationLoader start Dependency Injecting Actors and get tested?


In "Dependency Injecting Actors" it's shown how to inject a parameter into the constructor of a child actor. The parent actor uses injectedChild to be allowed to pass to the child (at child creation time) only the non-injected parameter and then let Guice inject the rest. To do this, it extends InjectedActorSupport and gets the child's factory injected in the constructor:

class MyParent @Inject() (childFactory: MyChild.Factory,
                           @Assisted something: Something,
                           @Assisted somethingElse: SomethingElse) extends Actor with InjectedActorSupport
[..]
    val child: ActorRef = injectedChild(childFactory(something, somethingElse), childName)

But what about the class that starts the parent and is not an actor but a custom ApplicationLoader? How can I start the parent actor from there? No mention of this is in the documentation.

I tried doing the same for the loader as I did for parent:

class MyLoader @Inject() (parentFactory: MyParent.Factory) extends ApplicationLoader with Actor with InjectedActorSupport {
[..]
val parent = injectedChild(parentFactory(something, somethingElse), parentName)

would this be correct? How can I test it?

class MyModule extends AbstractModule with AkkaGuiceSupport {
  def configure = {
    bindActor[MyParent](parentName)
    bindActor[MyLoader](loaderName)
    bindActorFactory[MyChild, MyChild.Factory]
    bindActorFactory[MyParent, MyParent.Factory]
  }
}

So:

  1. How do I start the parent from MyLoader while letting Guice dependency-inject what's required?
  2. How can I test MyLoader? This has been my test so far but now I need to pass the injected thingy to MyLoader and I don't know how (note the ***???**** in place of the argument which I do not know where to find):

    class MyLoaderSpec(_system: ActorSystem, implicit val ec: ExecutionContext) extends TestKit(_system) with WordSpecLike with BeforeAndAfterAll with Matchers { val loader = new SimstimLoader(???)

    override def beforeAll(): Unit = { loader.load(ApplicationLoader.createContext(new Environment(new File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test))) }

Thanks a million in advance!


Solution

  • Here is how I solved this issue.

    --> How to start a parent actor who needs dependency-injection. First of all, manually starting such an actor is impossible if you, like me, need to dependency-inject an instance which you do not know how to pass and where from. The solution is to let Guice start the actor automagically. Here is how. First, create your binder module for Guice:

    class MyModule extends AbstractModule with AkkaGuiceSupport{
    
      override def configure(): Unit = {
        bindActor[Root](Root.NAME)
        bind(classOf[StartupActors]).asEagerSingleton()
      }
    }
    

    Then, tell Play where your binder module is located by adding the following in your conf/application.conf:

    play.modules={
      enabled += "my.path.to.MyModule"
    }
    

    The StartupActors is simply a class I use to log whenever the automagic start of dependency-injected actors actually takes place. I log the event so that I can be sure of when and whether it occurs:

    class StartupActors @Inject() (@Named(Root.NAME) root: ActorRef) {
      play.api.Logger.info(s"Initialised $root")
    }
    

    The Root actor in my case takes care of parsing a custom configuration. Since the resulting vars from the parsing is required by my parent actor and during the tests I need to mock such resulting vars, I delegate the parsing to an actor other than the parent actor, i.e., the Root actor:

    object Root {
      final val NAME = "THERoot"
      case class ParseConfiguration()
    }
    
    class Root @Inject()(configuration: Configuration, projectDAO: ProjectDAO) extends Actor {
      val resultingVar: Something = myConfigParsing()
    
      override def preStart(): Unit = {
        context.actorOf(Props(new MyParent(resultingVar: Something, somethingElse: SomethingElse, projectDAO: ProjectDAO)))
      }
    
      override def receive: Receive = {
        case ParseConfiguration => sender ! myConfigParsing()
        case _ => logger.error("Root actor received an unsupported message")
      }
    }
    

    The ParseConfiguration message is used uniquely for testing purposes. Normally the configuration parsing occurs instead because of the initialisation of the resultingVar attribute.

    This way, MyParent wont need to get anything injected. Only StartupActors and Root will get injected. MyParent will simply get projectDAO from Root and pass it on to all its children.

    class MyParent(something: Something, somethingElse: SomethingElse, projectDAO: ProjectDAO) extends Actor { ... }
    

    Finally, for completion, I'm reporting here how I wrote the tests since I had troubles finding enough information online around this as well.

    import akka.actor.{ActorRef, ActorSystem, Props}
    import akka.testkit.{TestKit, TestProbe}
    import com.typesafe.config.ConfigFactory
    import org.mockito.Mockito.mock
    import org.scalatest.{BeforeAndAfterAll, WordSpecLike}
    import org.specs2.matcher.MustMatchers
    import play.api.Configuration
    import scala.concurrent.ExecutionContext
    
    class RootSpec(_system: ActorSystem) extends TestKit(_system)
      with WordSpecLike with BeforeAndAfterAll with MustMatchers {
    
      implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global
      val conf: com.typesafe.config.Config = ConfigFactory.load()
      val configuration: Configuration = Configuration(conf)
      val projectDAOMock: ProjectDAO = mock(classOf[ProjectDAO])
    
      private var mainActor: ActorRef = _
      private var something: Something = Something.empty
    
      def this() = this(ActorSystem("MySpec"))
    
      override def afterAll: Unit = {
        system.shutdown()
      }
    
      override def beforeAll(): Unit = {
        mainActor = system.actorOf(Props(new Root(configuration, projectDAOMock)), Root.NAME)
      }
    
      "RootSpec: Root Actor" should {
        val probe = TestProbe()
    
        "successfully parse the configuration file" in {
          probe.send(mainActor, ParseConfiguration)
          something = probe.expectMsgPF() {
            case msg => msg.asInstanceOf[Something]
          }
        }
      }
    }
    

    and then I test MyParent by conveniently providing mock objects in place of vars resulting from the configuration parsing:

    import akka.actor.{ActorRef, ActorSystem, Props}
    import akka.testkit.{TestKit, TestProbe}
    import org.mockito.Mockito
    import org.mockito.Mockito._
    import org.scalatest.{BeforeAndAfterAll, WordSpecLike}
    import org.specs2.matcher.MustMatchers
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.concurrent.{ExecutionContext, Future}
    
    case class AnyProjectAPI(val projectAPI: ProjectAPI) extends AnyVal
    class MyParentSpec(_system: ActorSystem, implicit val ec: ExecutionContext) extends TestKit(_system)
      with WordSpecLike with BeforeAndAfterAll with MustMatchers {
      val something = mock(classOf[Something])
      val somethingElse = mock(classOf[somethingElse])
      val projectDAOMock: ProjectDAO = mock(classOf[ProjectDAO])
    
      val projectTest: ProjectAPI = new ProjectAPI(allMyRandomConstructorArguments),
      val projectsList: List[ProjectAPI] = List(projectTest)
      val expectedCreationId = 1
      private var parent: ActorRef = _
    
      def this() = this(ActorSystem("MySpec"), scala.concurrent.ExecutionContext.global)
    
      override def afterAll: Unit = {
        system.shutdown()
      }
    
      override def beforeAll(): Unit = {
        parent = system.actorOf(Props(new MyParent(something, somethingElse, projectDAOMock)), MyParent.NAME)
      }
    
      "MyParentTesting: parent's pull request" should {
        when(myProjApi.getAllProjects).thenReturn(Future {projectsList})
        val anyProject: AnyProjectAPI = AnyProjectAPI(org.mockito.Matchers.any[ProjectAPI])
        Mockito.when(projectDAOMock.create(org.mockito.Matchers.any[ProjectAPI]))
          .thenReturn(Future {expectedCreationId}: Future[Int])
        val probe = TestProbe()
        val probe1 = TestProbe()
    
        "be successfully satisfied by all children when multiple senders are waiting for an answer" in {
          probe.send(parent, UpdateProjects)
          probe1.send(parent, UpdateProjects)
          allChildren.foreach(child =>
            probe.expectMsg(expectedCreationId))
          allChildren.foreach(child =>
            probe1.expectMsg(expectedCreationId))
        }
      }
    }