scalaakka-typed

Akka-Typed Actor unit tests


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

Solution

  • 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, Futures, 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.