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))
更简洁地说,您可以省略 Askable
、Future
和 ExecutionContext
,并使用 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
而没有扩展任何东西。
我一直在努力学习如何测试 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))
更简洁地说,您可以省略 Askable
、Future
和 ExecutionContext
,并使用 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
而没有扩展任何东西。