Synchronous behavior testing

You are viewing the documentation for the new actor APIs, to view the Pekko Classic documentation, see Classic Testing.

The BehaviorTestKit provides a very nice way of unit testing a Behavior in a deterministic way, but it has some limitations to be aware of.

Certain Behaviors will be hard to test synchronously and the BehaviorTestKit doesn’t support testing of all features. In those cases the asynchronous ActorTestKit is recommended. Example of limitations:

  • Spawning of Future or other asynchronous task and you rely on a callback to complete before observing the effect you want to test.
  • Usage of scheduler is not supported.
  • EventSourcedBehavior can’t be tested.
  • Interactions with other actors must be stubbed.
  • Blackbox testing style.
  • Supervision is not supported.

The BehaviorTestKit will be improved and some of these problems will be removed but it will always have limitations.

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
Java
sourceimport org.apache.pekko
import pekko.actor.testkit.typed.CapturedLogEvent
import pekko.actor.testkit.typed.Effect._
import pekko.actor.testkit.typed.scaladsl.BehaviorTestKit
import pekko.actor.testkit.typed.scaladsl.TestInbox
import pekko.actor.typed._
import pekko.actor.typed.scaladsl._
import com.typesafe.config.ConfigFactory
import org.slf4j.event.Level
sourceimport org.apache.pekko.actor.testkit.typed.CapturedLogEvent;
import org.apache.pekko.actor.testkit.typed.Effect;
import org.apache.pekko.actor.testkit.typed.javadsl.BehaviorTestKit;
import org.apache.pekko.actor.testkit.typed.javadsl.TestInbox;
import org.apache.pekko.actor.typed.*;
import org.apache.pekko.actor.typed.javadsl.*;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;

import com.typesafe.config.Config;
import org.slf4j.event.Level;

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

Scala
Java
sourceobject Hello {
  sealed trait Command
  case object CreateAnonymousChild extends Command
  case class CreateChild(childName: String) extends Command
  case class SayHelloToChild(childName: String) extends Command
  case object SayHelloToAnonymousChild extends Command
  case class SayHello(who: ActorRef[String]) extends Command
  case class LogAndSayHello(who: ActorRef[String]) extends Command

  def apply(): Behaviors.Receive[Command] = Behaviors.receivePartial {
    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
  }
sourcepublic static class Hello extends AbstractBehavior<Hello.Command> {

  public interface Command {}

  public static class CreateAChild implements Command {
    public final String childName;

    public CreateAChild(String childName) {
      this.childName = childName;
    }
  }

  public enum CreateAnAnonymousChild implements Command {
    INSTANCE
  }

  public static class SayHelloToChild implements Command {
    public final String childName;

    public SayHelloToChild(String childName) {
      this.childName = childName;
    }
  }

  public enum SayHelloToAnonymousChild implements Command {
    INSTANCE
  }

  public static class SayHello implements Command {
    public final ActorRef<String> who;

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

  public static class LogAndSayHello implements Command {
    public final ActorRef<String> who;

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

  public static Behavior<Command> create() {
    return Behaviors.setup(Hello::new);
  }

  private Hello(ActorContext<Command> context) {
    super(context);
  }

  @Override
  public Receive<Command> createReceive() {
    return newReceiveBuilder()
        .onMessage(CreateAChild.class, this::onCreateAChild)
        .onMessage(CreateAnAnonymousChild.class, this::onCreateAnonymousChild)
        .onMessage(SayHelloToChild.class, this::onSayHelloToChild)
        .onMessage(SayHelloToAnonymousChild.class, this::onSayHelloToAnonymousChild)
        .onMessage(SayHello.class, this::onSayHello)
        .onMessage(LogAndSayHello.class, this::onLogAndSayHello)
        .build();
  }

  private Behavior<Command> onCreateAChild(CreateAChild message) {
    getContext().spawn(Child.create(), message.childName);
    return Behaviors.same();
  }

  private Behavior<Command> onCreateAnonymousChild(CreateAnAnonymousChild message) {
    getContext().spawnAnonymous(Child.create());
    return Behaviors.same();
  }

  private Behavior<Command> onSayHelloToChild(SayHelloToChild message) {
    ActorRef<String> child = getContext().spawn(Child.create(), message.childName);
    child.tell("hello");
    return Behaviors.same();
  }

  private Behavior<Command> onSayHelloToAnonymousChild(SayHelloToAnonymousChild message) {
    ActorRef<String> child = getContext().spawnAnonymous(Child.create());
    child.tell("hello stranger");
    return Behaviors.same();
  }

  private Behavior<Command> onSayHello(SayHello message) {
    message.who.tell("hello");
    return Behaviors.same();
  }

  private Behavior<Command> onLogAndSayHello(LogAndSayHello message) {
    getContext().getLog().info("Saying hello to {}", message.who.path().name());
    message.who.tell("hello");
    return Behaviors.same();
  }
}

For creating a child actor a noop actor is created:

Scala
Java
sourceval childActor = Behaviors.receiveMessage[String] { _ =>
  Behaviors.same[String]
}
sourcepublic static class Child {
  public static Behavior<String> create() {
    return Behaviors.receive((context, message) -> 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
Java
sourceval testKit = BehaviorTestKit(Hello())
testKit.run(Hello.CreateChild("child"))
testKit.expectEffect(Spawned(childActor, "child"))
sourceBehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
test.run(new Hello.CreateAChild("child"));
assertEquals("child", test.expectEffectClass(Effect.Spawned.class).childName());

Anonymously:

Scala
Java
sourceval testKit = BehaviorTestKit(Hello())
testKit.run(Hello.CreateAnonymousChild)
testKit.expectEffect(SpawnedAnonymous(childActor))
sourceBehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
test.run(Hello.CreateAnAnonymousChild.INSTANCE);
test.expectEffectClass(Effect.SpawnedAnonymous.class);

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
Java
sourceval testKit = BehaviorTestKit(Hello())
val inbox = TestInbox[String]()
testKit.run(Hello.SayHello(inbox.ref))
inbox.expectMessage("hello")
sourceBehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
TestInbox<String> inbox = TestInbox.create();
test.run(new Hello.SayHello(inbox.getRef()));
inbox.expectMessage("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
Java
sourceval testKit = BehaviorTestKit(Hello())
testKit.run(Hello.SayHelloToChild("child"))
val childInbox = testKit.childInbox[String]("child")
childInbox.expectMessage("hello")
sourceBehaviorTestKit<Hello.Command> testKit = BehaviorTestKit.create(Hello.create());
testKit.run(new Hello.SayHelloToChild("child"));
TestInbox<String> childInbox = testKit.childInbox("child");
childInbox.expectMessage("hello");

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

Scala
Java
sourceval testKit = BehaviorTestKit(Hello())
testKit.run(Hello.SayHelloToAnonymousChild)
val child = testKit.expectEffectType[SpawnedAnonymous[String]]

val childInbox = testKit.childInbox(child.ref)
childInbox.expectMessage("hello stranger")
sourceBehaviorTestKit<Hello.Command> testKit = BehaviorTestKit.create(Hello.create());
testKit.run(Hello.SayHelloToAnonymousChild.INSTANCE);
// Anonymous actors are created as: $a $b etc
TestInbox<String> childInbox = testKit.childInbox("$a");
childInbox.expectMessage("hello stranger");

Testing other effects

The BehaviorTestKit keeps track other effects you can verify, look at the sub-classes of Effect

  • SpawnedAdapter
  • Stopped
  • Watched
  • WatchedWith
  • Unwatched
  • Scheduled
  • TimerScheduled
  • TimerCancelled

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
Java
sourceval testKit = BehaviorTestKit(Hello())
val inbox = TestInbox[String]("Inboxer")
testKit.run(Hello.LogAndSayHello(inbox.ref))
testKit.logEntries() shouldBe Seq(CapturedLogEvent(Level.INFO, "Saying hello to Inboxer"))
sourceBehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
TestInbox<String> inbox = TestInbox.create("Inboxer");
test.run(new Hello.LogAndSayHello(inbox.getRef()));

List<CapturedLogEvent> allLogEntries = test.getAllLogEntries();
assertEquals(1, allLogEntries.size());
CapturedLogEvent expectedLogEvent =
    new CapturedLogEvent(
        Level.INFO,
        "Saying hello to Inboxer",
        Optional.empty(),
        Optional.empty(),
        new HashMap<>());
assertEquals(expectedLogEvent, allLogEntries.get(0));

See the other public methods and API documentation on BehaviorTestKit for other types of verification.