In Akka Typed we need an ActorContext to create new actors.

This poses some problems if we want to create an actor inside a class that’s not an actor. We can pass around an ActorContext from a (higher level) actor. But if this is a longer-lived class, we have to keep in mind that this ActorContext is only valid during construction. So it’s generally frowned upon to pass around the ActorContext.

Because of this, Akka testKit doesn’t provide an ActorContext.

We can however spawn an Actor and take it’s context.

The class to test:

import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import Theater._
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.util.Timeout

import scala.concurrent.Future
import scala.concurrent.duration.DurationInt

class Theater(actorContext: ActorContext[_]) {
private val echoActor: ActorRef[Query] = actorContext.spawnAnonymous(echoBehavior)

  private val timeout = new Timeout(5 seconds)
  private val scheduler = actorContext.system.scheduler

  def echo(in: In): Future[Out] =
    echoActor.ask(ref => Query(in, ref))(timeout, scheduler)
}

object Theater {
type In = String
type Out = String

  case class Query(input: In, replyTo: ActorRef[Out])

  val echoBehavior: Behavior[Query] = Behaviors.setup { context =>
    Behaviors.receiveMessage {
      case Query(in, replyTo) =>
        val out = in
        replyTo ! out
        Behaviors.same
    }
  }
}

The test:

import akka.NotUsed
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.typed.{ActorRef, Behavior}
import org.scalatest.Assertion
import org.scalatest.wordspec.AnyWordSpecLike

import scala.concurrent.duration.DurationInt

class TheaterTest extends ScalaTestWithActorTestKit with AnyWordSpecLike {

  def runWithContext[T](f: ActorContext[T] => Assertion): Assertion = {
    def extractor(replyTo: ActorRef[Assertion]): Behavior[T] =
      Behaviors.setup { context =>
        replyTo ! f(context)

        Behaviors.ignore
      }
    val probe = testKit.createTestProbe[Assertion]()
    testKit.spawn(extractor(probe.ref))
    probe.receiveMessage(1.minute)
  }

  "Theater" should {
    "echo input back" in runWithContext[NotUsed] { context =>
      val theater = new Theater(context)

      whenReady(theater.echo("test")) { r =>
        r shouldBe "test"
      }
    }
  }
}

Notice that in this example class, we need the Scheduler from the ActorContext, for the ask function. So for this to work, the ActorContext needs to stay valid all the time we keep the Theater class around.

shadow-left