Saturday, February 12, 2011

Unit testing Akka Actors with the TestKit

Just a quick post to show some examples of how you can use the akka.util.TestKit to unit test Actors. I'm not sure when it was added to Akka but it is really handy. Instead of using CyclicBarriers and CountDownLatches, you just mixin this very handy trait into your spec, akka.util.TestKit.

I added some examples of test scenarios that pop up frequently when working with Actors:
  • You send something to an Actor and you would like to know if it received the message
  • You send something to an Actor and that in turn does some processing and on success sends something to another Actor and you would like to know if that Actor received a specific message
  • You send some messages and you are not interested in every message the Actor receives and would like to ignore them until some message is received
  • You would like to know for sure that an Actor does not send through certain messages to others
  • You would like to know what an Actor sends back as a response based on a certain message
Testing this by hand with the correct concurrent latches and barriers can be brittle and quite error prone, not to mention quite complex.

So how does it work? The TestKit sort of swaps out the senderOption of an Actor through an implicit value scoping trick, which means that when your Actor sends back a message (to "the sender", for instance with a reply), it automatically sends it back to a testActor in TestKit, which queues up messages for you so you can inspect stuff. You can also use that same testActor when you have an Actor that sends messages to some ActorRef. You just switch out the ActorRef you would normally pass to your Actor with the testActor Ref.

I added some very simple Actors for the above scenarios, which touch a lot of typical interactions:
  • EchoActor - an Actor that just echoes any message you send to it
  • ForwardingActor - an Actor that forwards a message to another Actor(Ref)
  • FilteringActor - an Actor that forwards only certain messages (of type String)
  • SequencingActor - an Actor that forwards some random amount of uninteresting messages, an interesting message, and then some random amount of uninteresting messages
As you can see in the code below, you use a within block to indicate that some piece of code must complete with a certain duration. Every within block gets handled sequentially to keep things simple. Some nice methods and implicits in akka.util.duration make it possible to just write 'within(100 millis)' to indicate a duration of 100 milliseconds, or 'within(50 millis, 100 millis)' to indicate a min and max for the duration.
Then you just bang out some messages on an ActorRef. After that you can use expectMsg to assert that a specific message has been received, or expectNoMsg to assert that no message should have been received.  'ignoreMsg' takes a partial function that ignores all messages for which the partial function returns true (Funny, the scala code explains it better than that sentence just now).  in the test with the SequencingActor, I ignore all messages that are not "something", than assert the "something" message and than ignore everything again that is not "something", making sure there are no messages after that. Make sure you stop all actors after all.
Anyway, enough rambling, check out this gist :)


Wednesday, February 9, 2011

Starting with Akka 1.0

Since version 1.0 of Akka is coming out very soon, I thought it might be handy to write a quick new post on getting started with Akka again since a couple of things have gotten quite a bit easier.

Development setup
I am using the following tools:
Install these tools by following the links. I am using sbt-idea as a sbt processor (see sbt-idea link). idea-sbt-plugin is a plugin that you can use to run sbt from within IDEA. Add the Scala plugin and idea-sbt-plugin (look for SBT) in IDEA by using the plugin manager (Settings-> Plugins).
After you have installed these tools you first need to create an sbt project. So lets do that first. ($ means I'm typing in the console, > means I'm in sbt, scala> means I'm in scala interpreter)


Minimal setup
$ mkdir akkastart
$ cd akkastart
$ sbt
press Y to start a new project, fill in some details like name, organization, version, scala version (2.8.1), sbt version (0.7.4) and wait for sbt to download scala and sbt dependencies.
This should create a lib, project, src and target directory. Create a project/build/ directory, add Project.scala to it with the following content:
import sbt._
import sbt_akka_bivy._

class MyProject(info: ProjectInfo) extends DefaultProject(info) with AkkaProject{
}

Create a project/plugins directory and add a Plugins.scala to it with the following content:
import sbt._

class Plugins(info: ProjectInfo) extends PluginDefinition(info) {
  val bumRepo = "Bum Networks Release Repository" at "http://repo.bumnetworks.com/releases"
  val AkkaRepo = "Akka Repository" at "http://akka.io/repository"
  val akkaPlugin = "se.scalablesolutions.akka" % "akka-sbt-plugin" % "1.0-RC6"
  val sbtAkkaBivy = "net.evilmonkeylabs" % "sbt-akka-bivy" % "0.2.0"
}

The akka plugin makes getting all the dependencies and modules for akka extremely easy. The bivy plugin is just really handy for deployment.
Then run sbt update. this will add a lib_managed directory with the minimal dependencies for Akka. add akka modules to the Project.scala file if you expect to use them, for instance akka-camel:

import sbt._
import sbt_akka_bivy._

class MyProject(info: ProjectInfo) extends DefaultProject(info) with AkkaProject{
  val akkaCamel = akkaModule("camel")
}

run sbt update after adding akka modules.
Now we can generate an idea project from the sbt project by running sbt idea: (if you followed installation instructions for sbt-idea as processor from the sbt-idea page)

$ sbt idea

That creates a .iml file your directory and a .idea directory with all the stuff Idea needs. (it also creates a .iml for building the sbt sources like Project.scala)

Now open the .iml file with Idea. Everything should just work (Don't you just love it when that happens? :)
Making your module (Build->Make Module akkastart) builds in the same target directories as sbt would.
Add an akka.conf to src/main/resources (so it gets on your classpath) if you don't like the defaults, check here on akka.io for more information. At first you probably don't need an akka.conf yet, when Akka finds no config it sets up defaults.

That's all there is to it, now you can start adding your Actors in src/main/scala as you see fit.
Of course you can also just do something like this to get that quick scala console fix (output in italics):

$ sbt
> console

scala> import akka.actor.Actor
import akka.actor.Actor
scala> class PrintActor extends Actor {
     |  def receive = {
     |   case s:String => {
     |     println("received:"+s)
     |   }
     |  }
     | }
defined class PrintActor
scala> val actorRef = Actor.actorOf(new PrintActor)
23:28:45.830 [run-main] WARN  akka.config.Config$ - 
Can't load 'akka.conf'.
One of the three ways of locating the 'akka.conf' file needs to be defined:
1. Define the '-Dakka.config=...' system property option.
2. Put the 'akka.conf' file on the classpath.
3. Define 'AKKA_HOME' environment variable pointing to the root of the Akka distribution.
I have no way of finding the 'akka.conf' configuration file.
Using default values everywhere.
actorRef: akka.actor.ActorRef = Actor[PrintActor:f55f3eb0-349b-11e0-ae19-0019d2b39ec9]
scala> actorRef.start   
23:28:59.710 [run-main] DEBUG a.d.Dispatchers$globalExecutorBasedEventDrivenDispatcher$ - Starting up Dispatchers$globalExecutorBasedEventDrivenDispatcher$[akka:event-driven:dispatcher:global]
with throughput [5]
res1: akka.actor.ActorRef = Actor[PrintActor:f55f3eb0-349b-11e0-ae19-0019d2b39ec9]
scala> actorRef ! "test"
23:29:03.412 [run-main] INFO  a.d.LazyExecutorServiceWrapper - Lazily initializing ExecutorService for 

scala> 23:29:03.428 [akka:event-driven:dispatcher:global-1] DEBUG akka.dispatch.MonitorableThread - Created thread akka:event-driven:dispatcher:global-1
received:test

For some more examples you can check these here on github, I've updated them to 1.0-RC6.


Happy hAkking!