Akka Actor Testing: TestKit, TestProbe, and akka-testkit Patterns
Akka actors are concurrent by nature — they communicate asynchronously through message passing, which makes traditional unit testing approaches fail. akka-testkit provides TestKit and TestProbe to write deterministic tests for actor behavior: message routing, state transitions, supervision strategies, and timing.
This guide covers the classic Akka (untyped) testkit and Akka Typed's BehaviorTestKit and ActorTestKit.
Setup
// build.sbt
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.8.0",
"com.typesafe.akka" %% "akka-testkit" % "2.8.0" % Test,
"org.scalatest" %% "scalatest" % "3.2.17" % Test
)For Akka Typed:
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % "2.8.0",
"com.typesafe.akka" %% "akka-actor-testkit-typed" % "2.8.0" % Test
)TestKit Basics
TestKit creates an ActorSystem for testing and provides the testActor — an actor you can send messages to and receive replies from:
import akka.actor.{ActorSystem, Props}
import akka.testkit.{ImplicitSender, TestKit}
import org.scalatest.BeforeAndAfterAll
import org.scalatest.wordspec.AnyWordSpecLike
class EchoActorSpec extends TestKit(ActorSystem("EchoSpec"))
with ImplicitSender
with AnyWordSpecLike
with BeforeAndAfterAll {
override def afterAll(): Unit = TestKit.shutdownSystem(system)
"EchoActor" should {
"echo messages back to sender" in {
val echo = system.actorOf(EchoActor.props)
echo ! "hello"
expectMsg("hello")
}
"handle multiple messages in order" in {
val echo = system.actorOf(EchoActor.props)
echo ! "first"
echo ! "second"
expectMsg("first")
expectMsg("second")
}
}
}ImplicitSender sets testActor as the implicit sender for all ! sends. Replies arrive at testActor and are retrieved with expectMsg.
Assertion Methods
// Exact match — waits up to 3 seconds by default
expectMsg("hello")
expectMsg(3.seconds, "hello") // custom timeout
// Type match
expectMsgType[UserCreated]
val event = expectMsgType[UserCreated]
assert(event.userId > 0)
// Predicate match
expectMsgPF() {
case UserCreated(id, _) if id > 0 => id
}
// No message expected
expectNoMessage(200.milliseconds)
// Any message
val msg = receiveOne(1.second)
// Multiple messages in any order
receiveN(3)
expectMsgAllOf("a", "b", "c") // all three, any order
// Ignore unwanted messages and wait for expected
fishForMessage(3.seconds) {
case "target" => true
case _ => false
}TestProbe
TestProbe creates additional "fake actors" to stand in for collaborators:
class OrderProcessorSpec extends TestKit(ActorSystem("OrderSpec"))
with ImplicitSender
with AnyWordSpecLike
with BeforeAndAfterAll {
override def afterAll(): Unit = TestKit.shutdownSystem(system)
"OrderProcessor" should {
"send confirmation to payment service" in {
val paymentService = TestProbe("payment")
val inventory = TestProbe("inventory")
val processor = system.actorOf(
OrderProcessor.props(paymentService.ref, inventory.ref)
)
processor ! PlaceOrder("item-1", quantity = 2, customerId = 42)
// Verify messages sent to collaborators
inventory.expectMsg(CheckStock("item-1", 2))
inventory.reply(StockAvailable("item-1", 2))
paymentService.expectMsgType[ChargeCard]
paymentService.reply(PaymentConfirmed("tx-123"))
expectMsg(OrderConfirmed("tx-123"))
}
}
}TestProbe is the key tool for verifying actor collaboration. It records all received messages and provides the same expectMsg API as TestKit.
Testing Actor State with Become/Unbecome
For actors that change behavior with context.become:
class TrafficLightSpec extends TestKit(ActorSystem("TrafficSpec"))
with ImplicitSender with AnyWordSpecLike with BeforeAndAfterAll {
override def afterAll(): Unit = TestKit.shutdownSystem(system)
"TrafficLight" should {
"transition from red to green" in {
val light = system.actorOf(TrafficLight.props)
light ! GetState
expectMsg(Red)
light ! Switch
light ! GetState
expectMsg(Green)
light ! Switch
light ! GetState
expectMsg(Yellow)
}
}
}Testing Supervision
Supervision strategies determine what happens when child actors fail. Test with TestProbe watching the actor:
"supervisor" should {
"restart child on ArithmeticException" in {
val supervisor = system.actorOf(Supervisor.props)
val probe = TestProbe()
supervisor ! CreateWorker("worker-1")
val worker = expectMsgType[ActorRef]
probe.watch(worker)
worker ! Divide(10, 0) // causes ArithmeticException
// Worker is restarted, not terminated
probe.expectNoMessage(500.milliseconds)
probe.expectNotTerminated(worker)
// Worker is back and functional
worker ! Divide(10, 2)
expectMsg(Result(5))
}
"stop child on unhandled exception" in {
val supervisor = system.actorOf(Supervisor.props)
val probe = TestProbe()
supervisor ! CreateWorker("worker-2")
val worker = expectMsgType[ActorRef]
probe.watch(worker)
worker ! FatalOperation
probe.expectTerminated(worker)
}
}Timed Assertions and Scheduling
"scheduler" should {
"fire heartbeat every second" in {
val probe = TestProbe()
val scheduler = system.actorOf(HeartbeatActor.props(probe.ref))
probe.expectMsg(2.seconds, Heartbeat)
probe.expectMsg(2.seconds, Heartbeat)
probe.expectMsg(2.seconds, Heartbeat)
}
}For deterministic time control, use akka.testkit.CallingThreadDispatcher or TestScheduler.
Akka Typed: BehaviorTestKit
Akka Typed provides BehaviorTestKit for synchronous, single-threaded behavior testing:
import akka.actor.testkit.typed.scaladsl.BehaviorTestKit
import akka.actor.testkit.typed.scaladsl.TestInbox
class CounterBehaviorSpec extends AnyWordSpec {
"Counter behavior" should {
"increment on Increment message" in {
val testKit = BehaviorTestKit(Counter())
val inbox = TestInbox[Int]()
testKit.run(Counter.Increment)
testKit.run(Counter.Increment)
testKit.run(Counter.GetCount(inbox.ref))
inbox.expectMessage(2)
}
"spawn child actor on CreateChild" in {
val testKit = BehaviorTestKit(ParentActor())
testKit.run(ParentActor.CreateChild("worker"))
// Verify child was spawned
assert(testKit.hasEffects())
testKit.expectEffectType[Effects.Spawned[_]]
}
}
}BehaviorTestKit is synchronous — all messages process immediately. Use it for pure behavior logic. For testing actual concurrency, use ActorTestKit.
Akka Typed: ActorTestKit
ActorTestKit creates a real ActorSystem for async testing:
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import org.scalatest.BeforeAndAfterAll
class UserRegistrySpec extends AnyWordSpec with BeforeAndAfterAll {
val testKit = ActorTestKit()
override def afterAll(): Unit = testKit.shutdownTestKit()
"UserRegistry" should {
"register and retrieve users" in {
val registry = testKit.spawn(UserRegistry())
val probe = testKit.createTestProbe[UserRegistry.Response]()
registry ! UserRegistry.Register("alice@example.com", probe.ref)
val created = probe.receiveMessage()
assert(created.isInstanceOf[UserRegistry.Registered])
registry ! UserRegistry.GetUser(created.asInstanceOf[UserRegistry.Registered].id, probe.ref)
val user = probe.receiveMessage().asInstanceOf[UserRegistry.UserFound]
assert(user.email == "alice@example.com")
}
}
}testKit.createTestProbe[T]() is the Typed equivalent of TestProbe.
Cluster and Persistence Testing
For Akka Persistence actors, use in-memory persistence:
// application.conf (test)
akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"
akka.persistence.snapshot-store.local.dir = "target/test-snapshots""PersistentCounter" should {
"recover state after restart" in {
val counter = system.actorOf(PersistentCounter.props("counter-1"))
counter ! Increment
counter ! Increment
counter ! GetCount
expectMsg(Count(2))
// Kill and recreate with same persistence ID
system.stop(counter)
Thread.sleep(100)
val recovered = system.actorOf(PersistentCounter.props("counter-1"))
recovered ! GetCount
expectMsg(Count(2)) // state recovered from journal
}
}Continuous Testing Beyond Unit Tests
akka-testkit verifies actor logic in isolation. For distributed Akka cluster behavior under real network conditions, HelpMeTest can run end-to-end scenarios against your live deployment — validating that message routing, supervision, and persistence work correctly in production.
Summary
akka-testkit provides the tools to write deterministic tests for inherently concurrent actor systems. TestKit gives you an actor-based test harness; TestProbe stands in for collaborators; BehaviorTestKit enables synchronous behavior testing for Akka Typed. Use expectMsg and its variants to assert on message sequences, and watch/expectTerminated to verify supervision outcomes.
Test actor behavior in isolation first, then integration-test the actor graph with real message flows. This layered approach catches both individual actor bugs and collaboration issues.