I've been trying to learn how to test akka-typed Actors. I've been referencing various examples online. I am successful at running the sample code but my efforts to write simple unit tests fail.
Can someone point out what I'm doing wrong? My goal is to be able to write unit test that verify each behavior.
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
}
}
CodePudding user response:
It's not really clear what problems you're encountering, so this answer is a bit of a shot in the dark with some observations.
You appear to be using the "command-then-query" pattern in testing this aspect of the behavior, which is OK (but see below for a different approach which I've found works really well). There are two basic ways you can approach this and your test looks like a bit of a mixture of the two in a way that probably is not working.
Regardless of approach, when sending the initial EatChocolate
message to the actor:
emotionActor ! EatChocolate
That message is sent to actor, not to the probe, so probe.expectMessage
won't succeed.
There are two flavors of ask in Akka Typed. There's a Future
-based one for outside of an actor, where the asking machinery injects a special ActorRef
to receive the reply and returns a Future
which will be completed when the reply is received. You can arrange for that Future
to send its result to the probe:
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))
More succinctly, you can dispense with Askable
, Future
s, and an ExecutionContext
and use probe.ref
as the replyTo
field in your HowHappy
message:
emotionActor ! HowHappy(probe.ref`)
probe.expectMessage(EmotionalFunctionalActor.Value(1))
This is more succinct and will probably be less flaky (being less prone to timing issues) than the Future
-based approach. Conversely, since the HowHappy
message appears designed for use with the ask pattern, the Future
-based approach may better fulfill a "test as documentation" purpose for describing how to interact with the actor.
If using the Future
-based approach with ScalaTest, it might be useful to have your suite extend akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
: this will provide some of the boilerplate and also mix in ScalaTest's ScalaFutures
trait, which would let you write something like
val replyFut: Future[EmotionalFunctionalActor.SimpleThing] = emotionActor ? HowHappy
assert(replyFut.futureValue == EmotionalFunctionalActor.Value(1))
I tend to prefer the BehaviorTestKit
, especially for cases where I'm not testing how two actors interact (this can still be done, it's just a bit laborious with the BehaviorTestKit
). This has the advantage of not having any timing at all and generally has less overhead for running tests.
val testKit = BehaviorTestKit(EmotionalFunctionalActor())
val testInbox = TestInbox[EmotionalFunctionalActor.SimpleThing]()
testKit.run(HowHappy(testInbox.ref))
testInbox.expectMessage(EmotionalFunctionalActor.Value(1))
As a side note, when designing a request-response protocol, it's generally a good idea to restrict the response type to the responses that might actually be sent, e.g.:
final case class HowHappy(replyTo: ActorRef[Value]) extends SimpleThing
This ensures that the actor can't reply with anything but a Value
message and means that the asker doesn't have to handle any other type of message. If there's a couple of different message types it could respond with, it might be worth having a trait
which is only extended (or mixed in) by those responses:
trait HappinessReply
final case class Value(happiness: Int) extends HappinessReply
final case class HowHappy(replyTo: ActorRef[HappinessReply]) extends SimpleThing
Further, the reply often won't make sense as a message received by the sending actor (as indicated in this case by it being handled by the "Received something I don't know" case). In this situation, Value
shouldn't extend SimpleThing
: it might even just be a bare case class
and not extend anything.