Home > Mobile >  Akka-Typed Actor unit tests
Akka-Typed Actor unit tests

Time:02-11

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

  • Related