自定义 ApplicationLoader 如何启动依赖注入 Actors 并进行测试?

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

在“Dependency Injecting Actors”中展示了如何将参数注入到子 actor 的构造函数中。父 actor 使用 injectedChild 允许仅将未注入的参数传递给子对象(在子对象创建时),然后让 Guice 注入其余参数。为此,它扩展了 InjectedActorSupport 并在构造函数中注入了子工厂:

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

但是启动父级并且不是 actor 而是自定义 ApplicationLoader 的 class 呢? 我怎样才能从那里开始父演员?文档中没有提到这一点。

我尝试对加载程序执行与对父级执行的操作相同的操作:

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

这是正确的吗?我怎样才能测试它?

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

所以:

  1. 如何在让 Guice 依赖注入所需内容的同时从 MyLoader 启动父级?
  2. 如何测试 MyLoader? 到目前为止,这是我的测试,但现在我需要将注入的东西传递给 MyLoader,我不知道如何(注意 ***???**** 代替我不知道在哪里的论点找到):

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

    重写 def beforeAll(): Unit = { loader.load(ApplicationLoader.createContext(新环境(新文件("."), ApplicationLoader.getClass.getClassLoader, Mode.Test))) }

提前致谢!

这是我解决这个问题的方法。

--> 如何启动需要依赖注入的父actor。 首先,如果你像我一样需要依赖注入一个你不知道如何传递和从哪里传递的实例,那么手动启动这样一个 actor 是不可能的。解决方案是让 Guice 自动启动 actor。方法如下。 首先,为 Guice 创建活页夹模块:

class MyModule extends AbstractModule with AkkaGuiceSupport{

  override def configure(): Unit = {
    bindActor[Root](Root.NAME)
    bind(classOf[StartupActors]).asEagerSingleton()
  }
}

然后,通过在 conf/application.conf 中添加以下内容,告诉 Play 您的活页夹模块所在的位置:

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

StartupActors 只是一个 class 我用它来记录依赖注入的 actor 的自动启动实际发生的时间。我记录该事件,以便确定它何时以及是否发生:

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

在我的案例中,Root actor 负责解析自定义配置。由于我的父 actor 需要解析的结果变量,并且在测试期间我需要模拟这些结果变量,我将解析委托给父 actor 以外的其他 actor,即 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")
  }
}

ParseConfiguration 消息专门用于测试目的。通常情况下,配置解析是由于 resultingVar 属性的初始化而发生的。

这样,MyParent 就不需要注入任何东西。只有 StartupActors 和 Root 会被注入。 MyParent 将简单地从 Root 获取 projectDAO 并将其传递给它的所有子级。

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

最后,为了完成,我在这里报告我是如何编写测试的,因为我也很难在网上找到足够的相关信息。

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]
      }
    }
  }
}

然后我通过方便地提供模拟对象来代替配置解析产生的变量来测试 MyParent:

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))
    }
  }
}