Actors are a really powerful tool to handle concurrency thanks to their message-based model. However, they can be tricky to test: sending and processing messages is done asynchronously. Moreover, their status is hidden internally and it cannot be easily accessed to make assertions on it.
The Akka Team has created a library, called akka-testkit, to simplify unit tests on actors. This article provides an overview of the main features of this library and how it can be used to test our lovely actors.
Single Threaded Tests
If our actor is particularly simple, a single threaded test may be enough. Thanks to TestActorRef, we are able to access the actor internal status and make assertions on it.
For example, we have built an actor that memorises all the received messages starting with ‘A’:
import akka.actor.Actor object MessageFilteringActorProtocol { case class SimpleMessage(text: String) } class MessageFilteringActor extends Actor { import MessageFilteringActorProtocol._ var messages = Vector[String]() // what the actor state is def state = messages // the actor behaviour when receiving an object def receive = { case SimpleMessage(text) if text startsWith "A" => messages = messages :+ text } }
Let’s build a test for our actor:
import akka.testkit.TestKit import akka.actor.ActorSystem import org.specs2.mutable.SpecificationLike class MessageFilteringActorSpec extends TestKit(ActorSystem()) with SpecificationLike { import MessageFilteringActorProtocol._ val actor = TestActorRef[MessageFilteringActor] "A Message Filtering Actor" should { "save only messages that starts with 'A'" in { actor ! SimpleMessage("A message to remember") actor ! SimpleMessage("This message should not be saved") actor ! SimpleMessage("Another message for you") actor.underlyingActor.state.length mustEqual 2 } } }
Multi Threaded Testing
Unfortunately, single threaded unit testing is not always sufficient with more complex scenarios. To perform multi threaded tests, we have access to the TestProbe class that offers useful methods to wait and analyse the status and interaction with our actor. Some of the most common methods are the following:
– expectMsg: it receives a message that is equal to the provided one
– expectNoMsg: it receives no message
– receiveWhile: it receives messages until the condition is respected or the time out is reached.
A complete list of all the methods offered by the TestProbe class can be found here.
Although the TestProbe class is quite powerful, it may require some changes in the actor code itself to make it more testable: we need to make sure that the actor is sending messages/information to our TestProbe class so that it can perform assertions on them.
A quite common approach is to create ad hoc messages for test purposes. For example, let’s assume we would like to know the internal status of our actor in a multi-threaded testing context. Moreover, we can have an optional listener to help us testing side effects.
An example on how to use these different approaches is as follows. Our BucketCounterActor prints the label on a bucket and it accumulates all the quantities received so far:
import akka.actor.Actor object BucketCounterActorProtocol { case class Bucket(label: String, quantity: Int) } class BucketCounterActor extends Actor { import BucketCounterActorProtocol._ var counter = 0 def receive = { case Bucket(label, quantity) => counter += quantity print(label) } }
Let’s add some ad hoc code to our actor for test purposes:
import akka.actor.{ActorRef, Actor} object BucketCounterActorProtocol { case class Bucket(label: String, quantity: Int) // a new message to expose the internal status of the actor case class GetCounter(receiver: ActorRef) } // adding an optional listener to the class class BucketCounterActor(listener: Option[ActorRef] = None) extends Actor { import BucketCounterActorProtocol._ var counter = 0 def receive = { case Bucket(label, quantity) => counter = counter + quantity print(label) // informing the listener of the side effect listener.map(_ ! label) // logic to expose internal status case GetCounter(receiver) => receiver ! counter } }
Thanks to the code we just added, testing our actor is now going to be really easy:
class BucketCounterActorSpec extends TestKit(ActorSystem()) with SpecificationLike { import BucketCounterActorProtocol._ "A Bucket Counter Actor" should { val actorProps = Props(new BucketCounterActor(Some(testActor))) val actor = system.actorOf(actorProps, "actor-to-test") val firstBucket = Bucket("Yo, I am a bucket", 1) val secondBucket = Bucket("I am another bucket", 9) "print out the name of the received buckets" in { actor ! firstBucket expectMsg(firstBucket.label) actor ! secondBucket expectMsg(secondBucket.label) success } "accumulate the quantity of buckets received" in { actor ! GetCounter(testActor) expectMsg(10) success } }
Summary
Akka actors are a powerful tool to build concurrent systems. This article has provided different examples on how actors can be tested thanks to the akka-testkit library, using both single and multi threaded approaches.
good refresher on what testActor is used for. thank you for the helpful example.
LikeLike
Hi nice reading your bllog
LikeLike