Pre-built actor implementations and factory methods for common testing patterns, including echo actors, blackhole actors, and message forwarding actors.
Collection of common test actor patterns and their factory methods.
object TestActors {
// Pre-built actor classes
class EchoActor extends Actor
class BlackholeActor extends Actor
class ForwardActor(target: ActorRef) extends Actor
// Factory Props
val echoActorProps: Props
val blackholeProps: Props
def forwardActorProps(target: ActorRef): Props
}Actor that echoes back all received messages to the sender.
class EchoActor extends Actor {
def receive = {
case message => sender() ! message
}
}Usage Example:
import akka.testkit.TestActors
class EchoActorTest extends TestKit(ActorSystem("TestSystem")) with ImplicitSender {
"EchoActor" should {
"echo back messages" in {
val echo = system.actorOf(TestActors.echoActorProps)
echo ! "hello"
expectMsg("hello")
echo ! 42
expectMsg(42)
echo ! List(1, 2, 3)
expectMsg(List(1, 2, 3))
}
"preserve sender reference" in {
val echo = system.actorOf(TestActors.echoActorProps)
val probe = TestProbe()
probe.send(echo, "test")
probe.expectMsg("test")
}
}
}Actor that ignores all messages (useful for dead-ending message flows).
class BlackholeActor extends Actor {
def receive = {
case _ => // Ignore all messages
}
}Usage Example:
class BlackholeActorTest extends TestKit(ActorSystem("TestSystem")) with ImplicitSender {
"BlackholeActor" should {
"ignore all messages" in {
val blackhole = system.actorOf(TestActors.blackholeProps)
blackhole ! "message1"
blackhole ! "message2"
blackhole ! 123
expectNoMessage(500.millis)
}
"be useful for message routing tests" in {
val blackhole = system.actorOf(TestActors.blackholeProps)
// Test that messages are routed to blackhole
val router = system.actorOf(Props(new Router(blackhole)))
router ! RouteToBlackhole("unwanted message")
expectNoMessage(200.millis) // Message was consumed by blackhole
}
}
}Actor that forwards all messages to a specified target actor.
class ForwardActor(target: ActorRef) extends Actor {
def receive = {
case message => target.forward(message)
}
}Usage Example:
class ForwardActorTest extends TestKit(ActorSystem("TestSystem")) with ImplicitSender {
"ForwardActor" should {
"forward messages to target" in {
val target = TestProbe()
val forwarder = system.actorOf(TestActors.forwardActorProps(target.ref))
forwarder ! "hello"
target.expectMsg("hello")
forwarder ! 42
target.expectMsg(42)
}
"preserve original sender" in {
val target = TestProbe()
val forwarder = system.actorOf(TestActors.forwardActorProps(target.ref))
val originalSender = TestProbe()
originalSender.send(forwarder, "test message")
val message = target.expectMsgType[String]
assert(message == "test message")
target.lastSender should be(originalSender.ref)
}
"be useful for proxy patterns" in {
val actualService = TestProbe()
val proxy = system.actorOf(TestActors.forwardActorProps(actualService.ref))
// Client thinks it's talking to the service directly
proxy ! ServiceRequest("data")
actualService.expectMsg(ServiceRequest("data"))
// Service responds back through proxy
actualService.reply(ServiceResponse("result"))
expectMsg(ServiceResponse("result"))
}
}
}Testing Message Flows:
class MessageFlowTest extends TestKit(ActorSystem("TestSystem")) with ImplicitSender {
"Message flow testing" should {
"verify routing with test actors" in {
val echo = system.actorOf(TestActors.echoActorProps, "echo")
val blackhole = system.actorOf(TestActors.blackholeProps, "blackhole")
val probe = TestProbe("target")
val forwarder = system.actorOf(TestActors.forwardActorProps(probe.ref), "forwarder")
// Create a router that routes based on message type
val router = system.actorOf(Props(new MessageRouter(
Map(
"echo" -> echo,
"ignore" -> blackhole,
"forward" -> forwarder
)
)))
// Test echo routing
router ! RouteMessage("echo", "hello")
expectMsg("hello")
// Test blackhole routing
router ! RouteMessage("ignore", "unwanted")
expectNoMessage(100.millis)
// Test forward routing
router ! RouteMessage("forward", "forwarded")
probe.expectMsg("forwarded")
}
}
}Load Testing with Test Actors:
class LoadTest extends TestKit(ActorSystem("TestSystem")) {
"Load testing" should {
"handle high message volume" in {
val echo = system.actorOf(TestActors.echoActorProps)
val messageCount = 1000
(1 to messageCount).foreach { i =>
echo ! s"message-$i"
}
val responses = receiveN(messageCount, 10.seconds)
responses should have size messageCount
responses.zipWithIndex.foreach { case (msg, i) =>
msg should be(s"message-${i + 1}")
}
}
}
}Integration Testing:
class IntegrationTest extends TestKit(ActorSystem("TestSystem")) {
"Integration with test actors" should {
"simulate external dependencies" in {
// Use ForwardActor to simulate external service
val externalServiceMock = TestProbe("external-service")
val serviceProxy = system.actorOf(
TestActors.forwardActorProps(externalServiceMock.ref),
"service-proxy"
)
// System under test uses the proxy
val businessLogic = system.actorOf(Props(new BusinessLogic(serviceProxy)))
businessLogic ! ProcessData("test-data")
// Verify external service call
externalServiceMock.expectMsg(ExternalServiceCall("test-data"))
// Mock response
externalServiceMock.reply(ExternalServiceResponse("processed"))
// Verify business logic processes response
expectMsg(DataProcessed("processed"))
}
}
}You can create custom test actors for specific testing scenarios:
// Custom test actor that counts messages
class CountingActor extends Actor {
private var count = 0
def receive = {
case "count" =>
sender() ! count
case _ =>
count += 1
}
}
// Custom test actor that delays responses
class DelayedEchoActor(delay: FiniteDuration) extends Actor {
import context.dispatcher
def receive = {
case message =>
val originalSender = sender()
context.system.scheduler.scheduleOnce(delay) {
originalSender ! message
}
}
}
// Custom test actor factory
object CustomTestActors {
def countingActorProps: Props = Props[CountingActor]()
def delayedEchoProps(delay: FiniteDuration): Props = Props(new DelayedEchoActor(delay))
}Use Appropriate Test Actors:
Combine with TestProbes: Test actors work well with TestProbe for complex scenarios
Name Your Actors: Use descriptive names for easier debugging
Clean Resource Usage: Test actors are lightweight but still consume resources
// Good: Descriptive names and appropriate usage
val echoService = system.actorOf(TestActors.echoActorProps, "echo-service")
val messageSync = system.actorOf(TestActors.blackholeProps, "message-sink")
val serviceProxy = system.actorOf(TestActors.forwardActorProps(realService), "service-proxy")
// Good: Combine with probes for verification
val probe = TestProbe("verification-probe")
val forwarder = system.actorOf(TestActors.forwardActorProps(probe.ref))