Akka 类型的 Actor 单元测试

Akka-Typed Actor unit tests

我一直在努力学习如何测试 akka 类型的 Actor。我一直在网上参考各种例子。我在 运行 示例代码方面取得了成功,但我编写简单单元测试的努力失败了。

有人可以指出我做错了什么吗?我的目标是能够编写验证每个行为的单元测试。

build.sbt

import Dependencies._

ThisBuild / scalaVersion := "2.13.7"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.example"
ThisBuild / organizationName := "example"

val akkaVersion = "2.6.18"

lazy val root = (project in file("."))
  .settings(
    name := "akkat",
    libraryDependencies ++= Seq(
      scalaTest % Test,
      "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
      "com.typesafe.akka" %% "akka-actor-testkit-typed" % akkaVersion % Test,
      "ch.qos.logback" % "logback-classic" % "1.2.3"
    )
  )

EmotionalFunctionalActor.scala

package example

import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.Behaviors

object EmotionalFunctionalActor {

  trait SimpleThing
  object EatChocolate extends SimpleThing
  object WashDishes extends SimpleThing
  object LearnAkka extends SimpleThing
  final case class Value(happiness: Int) extends SimpleThing
  final case class HowHappy(replyTo: ActorRef[SimpleThing]) extends SimpleThing

  def apply(happiness: Int = 0): Behavior[SimpleThing] = Behaviors.receive { (context, message) =>
    message match {
      case EatChocolate =>
        context.log.info(s"($happiness) eating chocolate")
        EmotionalFunctionalActor(happiness + 1)

      case WashDishes =>
        context.log.info(s"($happiness) washing dishes, womp womp")
        EmotionalFunctionalActor(happiness - 2)

      case LearnAkka =>
        context.log.info(s"($happiness) Learning Akka, yes!!")
        EmotionalFunctionalActor(happiness + 100)

      case HowHappy(replyTo) =>
        replyTo ! Value(happiness)
        Behaviors.same

      case _ =>
        context.log.warn("Received something i don't know")
        Behaviors.same
    }
  }
}

EmoSpec.scala

package example

import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.util.Timeout
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.concurrent.duration.DurationInt

class EmoSpec extends AnyFlatSpec
  with BeforeAndAfterAll
  with Matchers {

  val testKit = ActorTestKit()

  override def afterAll(): Unit = testKit.shutdownTestKit()

  "Happiness Leve" should "Increase by 1" in {
    val emotionActor = testKit.spawn(EmotionalFunctionalActor())
    val probe = testKit.createTestProbe[EmotionalFunctionalActor.SimpleThing]()

    implicit val timeout: Timeout = 2.second
    implicit val sched = testKit.scheduler
    import EmotionalFunctionalActor._

    emotionActor ! EatChocolate
    probe.expectMessage(EatChocolate)

    emotionActor ? HowHappy
    probe.expectMessage(EmotionalFunctionalActor.Value(1))

    val current = probe.expectMessageType[EmotionalFunctionalActor.Value]
    current shouldBe 1
  }
}

不太清楚您遇到了什么问题,所以这个答案有点凭空猜测。

您似乎在使用“command-then-query”模式来测试这方面的行为,这没问题(但请参阅下面的另一种方法,我发现它非常有效)。有两种基本方法可以解决这个问题,而您的测试看起来有点像两者的混合,可能不起作用。

无论采用何种方法,在向参与者发送初始 EatChocolate 消息时:

emotionActor ! EatChocolate

该消息被发送给 actor,而不是探测器,因此 probe.expectMessage 不会成功。

Akka Typed 中有两种风格的询问。在 actor 之外有一个基于 Future 的方法,其中请求机制注入一个特殊的 ActorRef 来接收回复,并且 returns 一个 Future 将在收到回复。您可以安排 Future 将其结果发送到探测器:

val testKit = ActorTestKit()
implicit val ec = testKit.system.executionContext

// after sending EatChocolate
val replyFut: Future[EmotionalFunctionalActor.SimpleThing] = emotionActor ? HowHappy
replyFut.foreach { reply =>
  probe.ref ! reply
}
probe.expectMessage(EmotionalFunctionalActor.Value(1))

更简洁地说,您可以省略 AskableFutureExecutionContext,并使用 probe.ref 作为 replyTo 字段 HowHappy 留言:

emotionActor ! HowHappy(probe.ref`)
probe.expectMessage(EmotionalFunctionalActor.Value(1))

与基于 Future 的方法相比,这种方法更简洁,而且可能不那么不稳定(不易出现时间问题)。相反,由于 HowHappy 消息似乎是为与询问模式一起使用而设计的,因此基于 Future 的方法可能会更好地实现“测试作为文档”的目的,以描述如何与参与者交互。

如果将基于 Future 的方法与 ScalaTest 一起使用,那么让您的套件扩展 akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit 可能会很有用:这将提供一些样板文件并混入 ScalaTest 的 ScalaFutures trait,它可以让你写出类似

的东西
val replyFut: Future[EmotionalFunctionalActor.SimpleThing] = emotionActor ? HowHappy
assert(replyFut.futureValue == EmotionalFunctionalActor.Value(1))

我更喜欢 BehaviorTestKit,尤其是在我不测试两个演员如何互动的情况下(这仍然可以做到,只是 BehaviorTestKit 有点费力)。这样做的好处是根本没有任何计时,并且通常 运行 测试的开销较小。

val testKit = BehaviorTestKit(EmotionalFunctionalActor())
val testInbox = TestInbox[EmotionalFunctionalActor.SimpleThing]()

testKit.run(HowHappy(testInbox.ref))
testInbox.expectMessage(EmotionalFunctionalActor.Value(1))

附带说明一下,在设计 request-response 协议时,通常最好将响应类型限制为可能实际发送的响应,例如:

final case class HowHappy(replyTo: ActorRef[Value]) extends SimpleThing

这确保了参与者只能用 Value 消息进行回复,这意味着提问者不必处理任何其他类型的消息。如果它可以响应几种不同的消息类型,那么 trait 可能是值得的,它仅由这些响应扩展(或混合):

trait HappinessReply
final case class Value(happiness: Int) extends HappinessReply
final case class HowHappy(replyTo: ActorRef[HappinessReply]) extends SimpleThing

此外,作为发送者收到的消息,回复通常没有意义(如本例中所示,它由“收到我不知道的东西”案例处理)。在这种情况下,Value 不应该扩展 SimpleThing:它甚至可能只是一个空 case class 而没有扩展任何东西。