Testing
Dependency
To use Akka TestKit Typed, add the module to your project:
- sbt
libraryDependencies += "com.typesafe.akka" %% "akka-actor-testkit-typed" % "2.5.32" % Test
- Maven
- Gradle
Introduction
Testing can either be done asynchronously using a real ActorSystem
or synchronously on the testing thread using the BehaviourTestKit
.
For testing logic in a Behavior
in isolation synchronous testing is preferred. For testing interactions between multiple actors a more realistic asynchronous test is preferred.
Certain Behavior
s will be hard to test synchronously e.g. if they spawn Future’s and you rely on a callback to complete before observing the effect you want to test. Further support for controlling the scheduler and execution context used will be added.
This module is ready to be used in production, but it is still marked as may change. This means that API or semantics can change without warning or deprecation period, but such changes will be collected and be performed in Akka 2.6.0 rather than in 2.5.x patch releases.
Synchronous behavior testing
The following demonstrates how to test:
- Spawning child actors
- Spawning child actors anonymously
- Sending a message either as a reply or to another actor
- Sending a message to a child actor
The examples below require the following imports:
- Scala
-
source
import akka.actor.testkit.typed.CapturedLogEvent import akka.actor.testkit.typed.Effect._ import akka.actor.testkit.typed.scaladsl.BehaviorTestKit import akka.actor.testkit.typed.scaladsl.TestInbox import akka.actor.typed._ import akka.actor.typed.scaladsl._ import akka.event.Logging
- Java
Each of the tests are testing an actor that based on the message executes a different effect to be tested:
- Scala
-
source
sealed trait Cmd case object CreateAnonymousChild extends Cmd case class CreateChild(childName: String) extends Cmd case class SayHelloToChild(childName: String) extends Cmd case object SayHelloToAnonymousChild extends Cmd case class SayHello(who: ActorRef[String]) extends Cmd case class LogAndSayHello(who: ActorRef[String]) extends Cmd val myBehavior = Behaviors.receivePartial[Cmd] { case (context, CreateChild(name)) => context.spawn(childActor, name) Behaviors.same case (context, CreateAnonymousChild) => context.spawnAnonymous(childActor) Behaviors.same case (context, SayHelloToChild(childName)) => val child: ActorRef[String] = context.spawn(childActor, childName) child ! "hello" Behaviors.same case (context, SayHelloToAnonymousChild) => val child: ActorRef[String] = context.spawnAnonymous(childActor) child ! "hello stranger" Behaviors.same case (_, SayHello(who)) => who ! "hello" Behaviors.same case (context, LogAndSayHello(who)) => context.log.info("Saying hello to {}", who.path.name) who ! "hello" Behaviors.same }
- Java
For creating a child actor a noop actor is created:
All of the tests make use of the BehaviorTestkit
to avoid the need for a real ActorContext
. Some of the tests make use of the TestInbox
which allows the creation of an ActorRef
that can be used for synchronous testing, similar to the TestProbe
used for asynchronous testing.
Spawning children
With a name:
- Scala
-
source
val testKit = BehaviorTestKit(myBehavior) testKit.run(CreateChild("child")) testKit.expectEffect(Spawned(childActor, "child"))
- Java
Anonymously:
- Scala
-
source
val testKit = BehaviorTestKit(myBehavior) testKit.run(CreateAnonymousChild) testKit.expectEffect(SpawnedAnonymous(childActor))
- Java
Sending messages
For testing sending a message a TestInbox
is created that provides an ActorRef
and methods to assert against the messages that have been sent to it.
- Scala
-
source
val testKit = BehaviorTestKit(myBehavior) val inbox = TestInbox[String]() testKit.run(SayHello(inbox.ref)) inbox.expectMessage("hello")
- Java
Another use case is sending a message to a child actor you can do this by looking up the ‘TestInbox’ for a child actor from the ‘BehaviorTestKit’:
- Scala
-
source
val testKit = BehaviorTestKit(myBehavior) testKit.run(SayHelloToChild("child")) val childInbox = testKit.childInbox[String]("child") childInbox.expectMessage("hello")
- Java
For anonymous children the actor names are generated in a deterministic way:
- Scala
-
source
val testKit = BehaviorTestKit(myBehavior) testKit.run(SayHelloToAnonymousChild) val child = testKit.expectEffectType[SpawnedAnonymous[String]] val childInbox = testKit.childInbox(child.ref) childInbox.expectMessage("hello stranger")
- Java
Testing other effects
The BehaviorTestkit
keeps track other effects you can verify, look at the sub-classes of akka.actor.testkit.typed.Effect
- SpawnedAdapter
- Stopped
- Watched
- Unwatched
- Scheduled
Checking for Log Messages
The BehaviorTestkit
also keeps track of everything that is being logged. Here, you can see an example on how to check if the behavior logged certain messages:
- Scala
-
source
val testKit = BehaviorTestKit(myBehavior) val inbox = TestInbox[String]("Inboxer") testKit.run(LogAndSayHello(inbox.ref)) testKit.logEntries() shouldBe Seq(CapturedLogEvent(Logging.InfoLevel, "Saying hello to Inboxer"))
- Java
See the other public methods and API documentation on BehaviorTestkit
for other types of verification.
Asynchronous testing
Asynchronous testing uses a real ActorSystem
that allows you to test your Actors in a more realistic environment.
The minimal setup consists of the test procedure, which provides the desired stimuli, the actor under test, and an actor receiving replies. Bigger systems replace the actor under test with a network of actors, apply stimuli at varying injection points and arrange results to be sent from different emission points, but the basic principle stays the same in that a single procedure drives the test.
Basic example
Actor under test:
- Scala
-
source
case class Ping(message: String, response: ActorRef[Pong]) case class Pong(message: String) val echoActor: Behavior[Ping] = Behaviors.receive { (_, message) => message match { case Ping(m, replyTo) => replyTo ! Pong(m) Behaviors.same } }
- Java
Tests create an instance of ActorTestKit
. This provides access to:
- An ActorSystem
- Methods for spawning Actors. These are created under the root guardian
- A method to shut down the ActorSystem from the test suite
- Scala
-
source
class AsyncTestingExampleSpec extends WordSpec with BeforeAndAfterAll with Matchers { val testKit = ActorTestKit()
- Java
Your test is responsible for shutting down the ActorSystem
e.g. using BeforeAndAfterAll
when using ScalaTest .
The following demonstrates:
- Creating a typed actor from the
TestKit
’s system usingspawn
- Creating a typed
TestProbe
- Verifying that the actor under test responds via the
TestProbe
- Scala
-
source
val pinger = testKit.spawn(echoActor, "ping") val probe = testKit.createTestProbe[Pong]() pinger ! Ping("hello", probe.ref) probe.expectMessage(Pong("hello"))
- Java
Actors can also be spawned anonymously:
Note that you can add import testKit._
to get access to the spawn
and createTestProbe
methods at the top level without prefixing them with testKit
.
Stopping actors
The method will wait until the actor stops or throw an assertion error in case of a timeout.
- Scala
-
source
val pinger1 = testKit.spawn(echoActor, "pinger") pinger1 ! Ping("hello", probe.ref) probe.expectMessage(Pong("hello")) testKit.stop(pinger1) // Uses default timeout // Immediately creating an actor with the same name val pinger2 = testKit.spawn(echoActor, "pinger") pinger2 ! Ping("hello", probe.ref) probe.expectMessage(Pong("hello")) testKit.stop(pinger2, 10.seconds) // Custom timeout
- Java
The stop
method can only be used for actors that were spawned by the same ActorTestKit
. Other actors will not be stopped by that method.
Observing mocked behavior
When testing a component (which may be an actor or not) that interacts with other actors it can be useful to not have to run the other actors it depends on. Instead, you might want to create mock behaviors that accept and possibly respond to messages in the same way the other actor would do but without executing any actual logic. In addition to this it can also be useful to observe those interactions to assert that the component under test did send the expected messages. This allows the same kinds of tests as untyped TestActor
/Autopilot
.
As an example, let’s assume we’d like to test the following component:
- Scala
-
source
case class Message(i: Int, replyTo: ActorRef[Try[Int]]) class Producer(publisher: ActorRef[Message])(implicit scheduler: Scheduler) { def produce(messages: Int)(implicit timeout: Timeout): Unit = { (0 until messages).foreach(publish) } private def publish(i: Int)(implicit timeout: Timeout): Future[Try[Int]] = { publisher.ask(ref => Message(i, ref)) } }
- Java
In our test, we create a mocked publisher
actor. Additionally we use Behaviors.monitor
with a TestProbe
in order to be able to verify the interaction of the producer
with the publisher
:
- Scala
-
source
import testKit._ // simulate the happy path val mockedBehavior = Behaviors.receiveMessage[Message] { msg => msg.replyTo ! Success(msg.i) Behaviors.same } val probe = testKit.createTestProbe[Message]() val mockedPublisher = testKit.spawn(Behaviors.monitor(probe.ref, mockedBehavior)) // test our component val producer = new Producer(mockedPublisher) val messages = 3 producer.produce(messages) // verify expected behavior for (i <- 0 until messages) { val msg = probe.expectMessageType[Message] msg.i shouldBe i }
- Java
Test framework integration
If you are using ScalaTest you can extend akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
to have the async test kit automatically shutdown when the test is complete. This is done in afterAll
from the BeforeAndAfterAll
trait. If you override that method you should call super.afterAll
to shutdown the test kit.
Note that the dependency on ScalaTest is marked as optional from the test kit module, so your project must explicitly include a dependency on ScalaTest to use this.
- Scala
-
source
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import org.scalatest.WordSpecLike class ScalaTestIntegrationExampleSpec extends ScalaTestWithActorTestKit with WordSpecLike { "Something" must { "behave correctly" in { val probe = createTestProbe[String]() // ... assertions etc. } } }
- Java
Controlling the scheduler
It can be hard to reliably unit test specific scenario’s when your actor relies on timing: especially when running many tests in parallel it can be hard to get the timing just right. Making such tests more reliable by using generous timeouts make the tests take a long time to run.
For such situations, we provide a scheduler where you can manually, explicitly advance the clock.
- Scala
-
source
import scala.concurrent.duration._ import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ManualTime import akka.actor.testkit.typed.scaladsl.TestProbe import akka.actor.typed.scaladsl.Behaviors import org.scalatest.WordSpecLike class ManualTimerExampleSpec extends ScalaTestWithActorTestKit(ManualTime.config) with WordSpecLike { val manualTime: ManualTime = ManualTime() "A timer" must { "schedule non-repeated ticks" in { case object Tick case object Tock val probe = TestProbe[Tock.type]() val behavior = Behaviors.withTimers[Tick.type] { timer => timer.startSingleTimer("T", Tick, 10.millis) Behaviors.receiveMessage { _ => probe.ref ! Tock Behaviors.same } } spawn(behavior) manualTime.expectNoMessageFor(9.millis, probe) manualTime.timePasses(2.millis) probe.expectMessage(Tock) manualTime.expectNoMessageFor(10.seconds, probe) } } }
- Java