Testing
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 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 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.
Dependency
To use Akka TestKit Typed, add the module to your project:
- sbt
libraryDependencies += "com.typesafe.akka" %% "akka-testkit-typed" % "2.5.11" % Test
- Maven
<dependency> <groupId>com.typesafe.akka</groupId> <artifactId>akka-testkit-typed_2.12</artifactId> <version>2.5.11</version> <scope>test</scope> </dependency>
- Gradle
dependencies { test group: 'com.typesafe.akka', name: 'akka-testkit-typed_2.12', version: '2.5.11' }
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
-
import akka.actor.typed._ import akka.actor.typed.scaladsl._ import akka.testkit.typed.scaladsl.Effects._
- Java
-
import akka.actor.typed.*; import akka.actor.typed.javadsl.*; import akka.testkit.typed.javadsl.*;
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 myBehavior = 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> myBehavior = 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(myBehavior) testKit.run(CreateChild("child")) testKit.expectEffect(Spawned(childActor, "child"))
- Java
-
BehaviorTestKit<Command> test = BehaviorTestKit.create(myBehavior); test.run(new CreateAChild("child")); test.expectEffect(Effects.spawned(childActor, "child", Props.empty()));
Anonymously:
- Scala
-
val testKit = BehaviorTestKit(myBehavior) testKit.run(CreateAnonymousChild) testKit.expectEffect(SpawnedAnonymous(childActor))
- Java
-
BehaviorTestKit<Command> test = BehaviorTestKit.create(myBehavior); test.run(new CreateAnAnonymousChild()); test.expectEffect(Effects.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(myBehavior) val inbox = TestInbox[String]() testKit.run(SayHello(inbox.ref)) inbox.expectMessage("hello")
- Java
-
BehaviorTestKit<Command> test = BehaviorTestKit.create(myBehavior); TestInbox<String> inbox = TestInbox.create(); test.run(new 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
-
val testKit = BehaviorTestKit(myBehavior) testKit.run(SayHelloToChild("child")) val childInbox = testKit.childInbox[String]("child") childInbox.expectMessage("hello")
- Java
-
BehaviorTestKit<Command> testKit = BehaviorTestKit.create(myBehavior); testKit.run(new SayHelloToChild("child")); TestInbox<String> childInbox = testKit.childInbox("child"); childInbox.expectMessage("hello");
For anonymous children the actor names are generated in a deterministic way:
- Scala
-
val testKit = BehaviorTestKit(myBehavior) testKit.run(SayHelloToAnonymousChild) // Anonymous actors are created as: $a $b etc val childInbox = testKit.childInbox[String](s"$$a") childInbox.expectMessage("hello stranger")
- Java
-
BehaviorTestKit<Command> testKit = BehaviorTestKit.create(myBehavior); testKit.run(new SayHelloToAnonymousChild()); // 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 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 ActorTestKit
. This provides access toTests create an instance of ActorTestKit
. This provides access to
- An ActorSystem
- Methods for spawning Actors. These are created under the root guardian
- A hook to shut down the ActorSystem from the test suite
- Scala
-
class AsyncTestingExampleSpec extends WordSpec with ActorTestKit with BeforeAndAfterAll {
- Java
-
public class AsyncTestingExampleTest { final static ActorTestKit testKit = ActorTestKit.create(AsyncTestingExampleTest.class);
Your test is responsible for shutting down the ActorSystem
e.g. using BeforeAndAfterAll
when using ScalaTest
- Scala
-
override def afterAll(): Unit = shutdownTestKit()
- Java
-
@AfterClass public void cleanup() { testKit.shutdownTestKit(); }
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
-
val probe = TestProbe[Pong]() val pinger = spawn(echoActor, "ping") pinger ! Ping("hello", probe.ref) probe.expectMessage(Pong("hello"))
- Java
-
TestProbe<Pong> probe = testKit.createTestProbe(); ActorRef<Ping> pinger = testKit.spawn(echoActor, "ping"); pinger.tell(new Ping("hello", probe.ref())); probe.expectMessage(new Pong("hello"));
Actors can also be spawned anonymously:
- Scala
-
val probe = TestProbe[Pong]() val pinger = spawn(echoActor) pinger ! Ping("hello", probe.ref) probe.expectMessage(Pong("hello"))
- Java
-
TestProbe<Pong> probe = testKit.createTestProbe(); ActorRef<Ping> pinger = testKit.spawn(echoActor); pinger.tell(new Ping("hello", probe.ref())); probe.expectMessage(new Pong("hello"));
Test framework integration
If you are using JUnit you can use akka.testkit.typed.javadsl.TestKitJunitResource
to have the async test kit automatically shutdown when the test is complete.
Note that the dependency on JUnit is marked as optional from the test kit module, so your project must explicitly include a dependency on JUnit to use this.
It often makes sense to introduce a common base class for all tests using the async test kit, here is an example how to hook it into a ScalaTest test suite.
- Scala
-
import akka.testkit.typed.scaladsl.ActorTestKit import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec } abstract class AbstractActorSpec extends WordSpec with ActorTestKit with Matchers with BeforeAndAfterAll { override protected def afterAll(): Unit = { shutdownTestKit() } }
- Java
-
import akka.testkit.typed.javadsl.TestKitJunitResource; import org.junit.ClassRule; import org.junit.Test; public class JunitIntegrationExampleTest { @ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource(); @Test public void testSomething() { TestProbe<String> probe = testKit.createTestProbe(); // ... assertions etc. } }
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
-
import akka.actor.typed.scaladsl.Behaviors import org.scalatest.WordSpecLike import scala.concurrent.duration._ class ManualTimerExampleSpec extends AbstractActorSpec { override def config = ManualTime.config val 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.immutable { (ctx, Tick) ⇒ 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
-
import java.util.concurrent.TimeUnit; import akka.actor.typed.Behavior; import akka.testkit.typed.javadsl.ManualTime; import akka.testkit.typed.javadsl.TestKitJunitResource; import org.junit.ClassRule; import org.scalatest.junit.JUnitSuite; import scala.concurrent.duration.Duration; import akka.actor.typed.javadsl.Behaviors; import org.junit.Test; import akka.testkit.typed.javadsl.TestProbe; public class ManualTimerExampleTest extends JUnitSuite { @ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource(ManualTime.config()); private final ManualTime manualTime = ManualTime.get(testKit.system()); static final class Tick {} static final class Tock {} @Test public void testScheduleNonRepeatedTicks() { TestProbe<Tock> probe = testKit.createTestProbe(); Behavior<Tick> behavior = Behaviors.withTimers(timer -> { timer.startSingleTimer("T", new Tick(), Duration.create(10, TimeUnit.MILLISECONDS)); return Behaviors.immutable( (ctx, tick) -> { probe.ref().tell(new Tock()); return Behaviors.same(); }); }); testKit.spawn(behavior); manualTime.expectNoMessageFor(Duration.create(9, TimeUnit.MILLISECONDS), probe); manualTime.timePasses(Duration.create(2, TimeUnit.MILLISECONDS)); probe.expectMessageClass(Tock.class); manualTime.expectNoMessageFor(Duration.create(10, TimeUnit.SECONDS), probe); } }