Testing

Warning

This module is currently marked as may change in the sense of being the subject of active research. This means that API or semantics can change without warning or deprecation period and it is not recommended to use this module in production just yet—you have been warned.

To use the testkit add the following dependency:

sbt
libraryDependencies += "com.typesafe.akka" %% "akka-testkit-typed" % "2.5-SNAPSHOT" % Test
Maven
<dependency>
  <groupId>com.typesafe.akka</groupId>
  <artifactId>akka-testkit-typed_2.12</artifactId>
  <version>2.5-SNAPSHOT</version>
  <scope>test</scope>
</dependency>
Gradle
dependencies {
  test group: 'com.typesafe.akka', name: 'akka-testkit-typed_2.12', version: '2.5-SNAPSHOT'
}

Testing can either be done asynchronously using a real ActorSystem or synchronously on the testing thread using the BehaviousTestKit.

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

Synchronous behaviour 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
import akka.actor.typed._
import akka.actor.typed.scaladsl._
import akka.testkit.typed._
import akka.testkit.typed.Effect._
Java
import akka.actor.typed.*;
import akka.actor.typed.javadsl.*;
import akka.testkit.typed.*;

Each of the tests are testing an actor that based on the message executes a different effect to be tested:

Scala
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

val myBehaviour = Behaviors.immutablePartial[Cmd] {
  case (ctx, CreateChild(name)) ⇒
    ctx.spawn(childActor, name)
    Behaviors.same
  case (ctx, CreateAnonymousChild) ⇒
    ctx.spawnAnonymous(childActor)
    Behaviors.same
  case (ctx, SayHelloToChild(childName)) ⇒
    val child: ActorRef[String] = ctx.spawn(childActor, childName)
    child ! "hello"
    Behaviors.same
  case (ctx, SayHelloToAnonymousChild) ⇒
    val child: ActorRef[String] = ctx.spawnAnonymous(childActor)
    child ! "hello stranger"
    Behaviors.same
  case (_, SayHello(who)) ⇒
    who ! "hello"
    Behaviors.same
Java
interface Command { }
public static class CreateAChild implements Command {
  private final String childName;
  public CreateAChild(String childName) {
    this.childName = childName;
  }
}
public static class CreateAnAnonymousChild implements Command { }
public static class SayHelloToChild implements Command {
  private final String childName;
  public SayHelloToChild(String childName) {
    this.childName = childName;
  }
}
public static class SayHelloToAnonymousChild implements Command { }
public static class SayHello implements Command {
  private final ActorRef<String> who;

  public SayHello(ActorRef<String> who) {
    this.who = who;
  }
}

public static Behavior<Command> myBehaviour = Behaviors.immutable(Command.class)
  .onMessage(CreateAChild.class, (ctx, msg) -> {
    ctx.spawn(childActor, msg.childName);
    return Behaviors.same();
  })
  .onMessage(CreateAnAnonymousChild.class, (ctx, msg) -> {
    ctx.spawnAnonymous(childActor);
    return Behaviors.same();
  })
  .onMessage(SayHelloToChild.class, (ctx, msg) -> {
    ActorRef<String> child = ctx.spawn(childActor, msg.childName);
    child.tell("hello");
    return Behaviors.same();
  })
  .onMessage(SayHelloToAnonymousChild.class, (ctx, msg) -> {
    ActorRef<String> child = ctx.spawnAnonymous(childActor);
    child.tell("hello stranger");
    return Behaviors.same();
  }).onMessage(SayHello.class, (ctx, msg) -> {
    msg.who.tell("hello");
    return Behaviors.same();
  }).build();

For creating a child actor a noop actor is created:

Scala
val childActor = Behaviors.immutable[String] { (_, _) ⇒
  Behaviors.same[String]
}
Java
public static Behavior<String> childActor = Behaviors.immutable((ctx, msg) -> Behaviors.same());

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
val testKit = BehaviorTestkit(myBehaviour)
testKit.run(CreateChild("child"))
testKit.expectEffect(Spawned(childActor, "child"))
Java
BehaviorTestkit<Command> test = BehaviorTestkit.create(myBehaviour);
test.run(new CreateAChild("child"));
test.expectEffect(new Effect.Spawned(childActor, "child", Props.empty()));

Anonymously:

Scala
val testKit = BehaviorTestkit(myBehaviour)
testKit.run(CreateAnonymousChild)
testKit.expectEffect(SpawnedAnonymous(childActor))
Java
BehaviorTestkit<Command> test = BehaviorTestkit.create(myBehaviour);
test.run(new CreateAnAnonymousChild());
test.expectEffect(new Effect.SpawnedAnonymous(childActor, Props.empty()));

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
val testKit = BehaviorTestkit(myBehaviour)
val inbox = TestInbox[String]()
testKit.run(SayHello(inbox.ref))
inbox.expectMsg("hello")
Java
BehaviorTestkit<Command> test = BehaviorTestkit.create(myBehaviour);
TestInbox<String> inbox = new TestInbox<String>();
test.run(new SayHello(inbox.ref()));
inbox.expectMsg("hello");

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
val testKit = BehaviorTestkit(myBehaviour)
testKit.run(SayHelloToChild("child"))
val childInbox = testKit.childInbox[String]("child")
childInbox.expectMsg("hello")
Java
BehaviorTestkit<Command> testKit = BehaviorTestkit.create(myBehaviour);
testKit.run(new SayHelloToChild("child"));
TestInbox<String> childInbox = testKit.childInbox("child");
childInbox.expectMsg("hello");

For anonymous children the actor names are generated in a deterministic way:

Scala
val testKit = BehaviorTestkit(myBehaviour)
testKit.run(SayHelloToAnonymousChild)
// Anonymous actors are created as: $a $b etc
val childInbox = testKit.childInbox[String]("$a")
childInbox.expectMsg("hello stranger")
Java
BehaviorTestkit<Command> testKit = BehaviorTestkit.create(myBehaviour);
testKit.run(new SayHelloToAnonymousChild());
// Anonymous actors are created as: $a $b etc
TestInbox<String> childInbox = testKit.childInbox("$a");
childInbox.expectMsg("hello stranger");

Testing other effects

The BehaviorTestkit keeps track other effects you can verify, look at the sub-classes of akka.testkit.typed.Effect

  • SpawnedAdapter
  • Stopped
  • Watched
  • Unwatched
  • Scheduled

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
case class Ping(msg: String, response: ActorRef[Pong])
case class Pong(msg: String)

val echoActor = Behaviors.immutable[Ping] { (_, msg) ⇒
  msg match {
    case Ping(m, replyTo) ⇒
      replyTo ! Pong(m)
      Behaviors.same
  }
}
Java
public static class Ping {
  private String msg;
  private ActorRef<Pong> replyTo;

  public Ping(String msg, ActorRef<Pong> replyTo) {
    this.msg = msg;
    this.replyTo = replyTo;
  }
}
public static class Pong {
  private String msg;

  public Pong(String msg) {
    this.msg = msg;
  }
}

Behavior<Ping> echoActor = Behaviors.immutable((ctx, ping) -> {
  ping.replyTo.tell(new Pong(ping.msg));
  return Behaviors.same();
});

Tests extend TestKit or include the TestKitBase. This provides access to * An ActorSystem * Methods for spawning Actors. These are created under the root guardian * Methods for creating system actors

Scala
class BasicAsyncTestingSpec extends TestKit("BasicTestingSpec")
  with WordSpecLike with BeforeAndAfterAll {
Java
public class BasicAsyncTestingTest extends TestKit {

Your test is responsible for shutting down the ActorSystem e.g. using BeforeAndAfterAll when using ScalaTest

Scala
override def afterAll(): Unit = shutdown()
Java
@AfterClass
public void cleanup() {
  this.shutdown();
}

The following demonstrates:

  • Creating a typed actor from the TestKit’s system using spawn
  • Creating a typed TestProbe
  • Verifying that the actor under test responds via the TestProbe
Scala
val probe = TestProbe[Pong]()
val pinger = spawn(echoActor, "ping")
pinger ! Ping("hello", probe.ref)
probe.expectMsg(Pong("hello"))
Java
TestProbe<Pong> probe = new TestProbe<>(system());
ActorRef<Ping> pinger = spawn(echoActor, "ping");
pinger.tell(new Ping("hello", probe.ref()));
probe.expectMsg(new Pong("hello"));

Actors can also be spawned anonymously:

Scala
val probe = TestProbe[Pong]()
val pinger = spawn(echoActor)
pinger ! Ping("hello", probe.ref)
probe.expectMsg(Pong("hello"))
Java
TestProbe<Pong> probe = new TestProbe<>(system());
ActorRef<Ping> pinger = spawn(echoActor);
pinger.tell(new Ping("hello", probe.ref()));
probe.expectMsg(new Pong("hello"));
Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.